effectfully / STLC

Dependently typed Algorithm M and friends

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

STLC-in-Agda

A quick taste

Getting a universe polymorphic Agda function from a pure lambda term:

phoenix : Syntax⁽⁾
phoenix = 4 # λ a b c d → a · (b · d) · (c · d)

phoenixᵗ : Term ((b ⇒ c ⇒ d) ⇒ (a ⇒ b) ⇒ (a ⇒ c) ⇒ a ⇒ d)
phoenixᵗ = term⁻ phoenix

liftM2 : ∀ {α β γ δ} {A : Set α} {B : Set β} {C : Set γ} {D : Set δ}
       -> ((B -> C -> D) -> (A -> B) -> (A -> C) -> A -> D)
liftM2 = eval phoenixᵗ

C-c C-n liftM2 gives λ {.α} {.β} {.γ} {.δ} {.A} {.B} {.C} {.D} x x₁ x₂ x₃ → x (x₁ x₃) (x₂ x₃).

Overview

This is simply typed lambda calculus with type variables in Agda. We have raw Syntax:

data Syntax n : Set where
  var : Fin n -> Syntax n
  ƛ_  : Syntax (suc n) -> Syntax n
  _·_ : Syntax n -> Syntax n -> Syntax n

typed terms:

data _⊢_ {n} Γ : Type n -> Set where
  var : ∀ {σ}   -> σ ∈ Γ     -> Γ ⊢ σ
  ƛ_  : ∀ {σ τ} -> Γ ▻ σ ⊢ τ -> Γ ⊢ σ ⇒ τ
  _·_ : ∀ {σ τ} -> Γ ⊢ σ ⇒ τ -> Γ ⊢ σ     -> Γ ⊢ τ

and a mapping from the former to the latter via a type-safe version of algorithm M:

M : ∀ {n l} -> (Γ : Con n l) -> Syntax l -> (σ : Type n)
  -> Maybe (∃ λ m -> ∃ λ (Ψ : Subst n m) -> mapᶜ (apply Ψ) Γ ⊢ apply Ψ σ)

M receives a context, a term and a type, and checks, whether there is a substitution that allows to typify the term in this context and with this type, after the substitution is applied to them. M uses rewrite rules under the hood — this simplifies the definition a lot.

There is an NbE, which uses the traversal from [5].

There is a part of the liftable terms approach to NbE (described in [4]), which is used to coerce Agda's lambda terms to their first-order counterparts.

There is a universe polymorphic eval.

Using all this we can, for example, make an Agda lambda term universe polymorphic:

mono-app : {A B : Set} -> (A -> B) -> A -> B
mono-app f x = f x

poly-app : ∀ {α β} {A : Set α} {B : Set β} -> (A -> B) -> A -> B
poly-app = eval (read (inst 2 λ A B -> mono-app {A} {B}))

Details

I turned off the eta rule for products, because Agda lacks sharing. My Agda is out of date a bit, so I redefined Σ as a data instead of placing {-# NO_ETA Σ #-} somewhere. Algorithm M is still incredibly slow, and the current _>>=ᵀ_ eats twice the memory comparing to the previous version, which was more performant, but less usable and inference-friendly, which is important because types signatures are quite verbose.

The Main module contains

on-typed : ∀ {α} {A : ∀ {n} {σ : Type n} -> Term⁽⁾ σ -> Set α}
         -> (f : ∀ {n} {σ : Type n} -> (t : Term⁽⁾ σ) -> A t) -> ∀ e -> _
on-typed f e = fromJustᵗ $ infer e >>=ᵗ f ∘ thicken ∘ core ∘ proj₂ ∘ proj₂

typed = on-typed $ id
term  = on-typed $ λ t {m Δ}   -> generalize {m} Δ t
term⁻ = on-typed $ λ {n} t {Δ} -> generalize {n} Δ t
normᵖ = on-typed $ pure ∘ erase ∘ norm

on-typed receives a function f and a term, tries to typify the term, makes type variables consecutive and strengthened, applies f to the result and removes just, when inferring is successful, or returns lift tt otherwise.

_>>=ᵗ_ constructs values of type mx >>=ᵀ B, which are "either nothing or B x". The idea is described here (I changed the definition a bit though).

thicken is defined in terms of enumerate described here.

generalize substitutes type variables with universally quantified types with universally quantified upper bounds for variables and prepends a universally quantified context:

app : ε {2} ⊢ (Var zero ⇒ Var (suc zero)) ⇒ Var zero ⇒ Var (suc zero)
app = ƛ ƛ var (vs vz) · var vz

gapp : ∀ {n σ τ} {Γ : Con n} -> Γ ⊢ (σ ⇒ τ) ⇒ σ ⇒ τ
gapp = generalize _ app

normᵖ normalizes a pure lambda term whenever it's typeable. Uses NbE under the hood.

Unsafeties

Two simple lemmas are used for for rewriting via the REWRITE pragma:

apply-apply :  {n m p} {Φ : Subst m p} {Ψ : Subst n m} σ
            -> apply Φ (apply Ψ σ) ≡ apply (Φ ∘ˢ Ψ) σ
apply-apply (Var i) = refl
apply-apply (σ ⇒ τ) = cong₂ _⇒_ (apply-apply σ) (apply-apply τ)

mapᶜ-mapᶜ :  {n m p l} {g : Type m -> Type p} {f : Type n -> Type m} (Γ : Con n l)
          -> mapᶜ g (mapᶜ f Γ) ≡ mapᶜ (g ∘ f) Γ
mapᶜ-mapᶜ  ε      = refl
mapᶜ-mapᶜ (Γ ▻ σ) = cong (_▻ _) (mapᶜ-mapᶜ Γ)

In one of the previous version I avoided rewriting, but the code was unreadable.

Unification is postulated to be TERMINATING:

{-# TERMINATING #-}
unify :  {n} -> (σ τ : Type n) -> Maybe (∃ λ: Subst n n) -> apply Ψ σ ≡ apply Ψ τ)

It can be proved so using the techniques from [6].

There are highly unsafe things in the STLC.Experimental.Unsafe module, but they are not used in the Algotihm M itself, only to define on-typed and its derivatives presented above.

References

  1. Martin Grabmüller. Algorithm W Step by Step
  2. Oukseh Lee, Kwangkeun Yi. A Generalized Let-Polymorphic Type Inference Algorithm
  3. Dr. Gergő Érdi. Compositional Type Checking
  4. Andreas Abel. Normalization by Evaluation: Dependent Types and Impredicativity
  5. Guillaume Allais. Type and Scope Preserving Semantics
  6. Conor McBride. First-Order Unification by Structural Recursion

About

Dependently typed Algorithm M and friends


Languages

Language:Agda 100.0%