Dependent type

Table of Contents

1. Dependently typed programming languages

  • Rocq (1989)
  • ATS (Applied Type System) (2006)
  • Idris (2007)
  • Agda (1999)
    • Agda 2 (2007) is a new language
  • Epigram (2004)
    • the syntax looks cursed… it's 2D!
  • F* (2011)
    • from Microsoft Research and Inria
  • Lean (2013)
    • by Leonardo de Moura (!) at Microsoft Research

2. TODO Dependently typed pattern matching

3. More things to explore

  • homotopy type theory (HoTT)
  • refinement types
    • type + predicate

4. Stuff from djeis on lisp discord (paraphrased or not)

https://discord.com/channels/297478281278652417/377481417334063105/1345545063119327272

  • a type theory with dependent types is a proof system
  • having a value is a kind of proof that whatever type that value has, does have elements, that the type is not empty.
  • with dependent types, you can express types that are only nonempty if some corresponding statement in logic is provable
  • And the value you construct to show that type is nonempty is a proof of the corresponding logical statement.
  • about quantifiers:
    • forall lets you express that the [later argument types and] return type of a function varies based on the value of its inputs
    • exists lets you express that
      • one of the fields of a record
      • has a type that varies based on the
      • value of another field.
  • In dependent type theories, a type can be passed around like a value and those A and B become real arguments.
    • So you might see something like map int string intToString foo
  • type signature for a length-preserving map:
    • forall (A B : Type) (n : int), (A -> B) -> Vector A n -> Vector B n
    • n doesn't have to be a compile-time constant
    • but the compiler must be able to statically recognize that n will match the length of the input vector at runtime
    • Which practically means it needs to be involved in constructing that vector, one way or another.
struct {
  size_t l;
  int[l] v;
}

When you construct a value of that struct, the compiler will only let you provide an l and a v that it can statically tell will have that length relationship, even if it doesn't know the specific length statically.

  • you can prove an induction by writing a recursive function

4.1. BHK interpretation

  • Brouwer–Heyting–Kolmogorov interpretation of intuitionistic logic
  • https://en.m.wikipedia.org/wiki/Brouwer%E2%80%93Heyting%E2%80%93Kolmogorov_interpretationIt is the standard explanation of intuitionistic logic.
    • "It is the standard explanation of intuitionistic logic."
    • "not" P means that P leads to absurdity
      • to avoid Gödel ;)
  • To summarize and get it out of the way:
    • and and or are the pair and sum types, respectively.
    • Evidence that A and B are both nonempty?
      • A pair of an A and a B.
    • Evidence that A or B is nonempty?
      • A value that is either holding an A or holding a B.

4.2. Generalization for ADTs to Inductive types

Where this gets interesting is how dependent types generalize ADTs into Inductive types.

4.2.1. Maybe type

So in a lot of FP langs you have polymorphic algebraic data types like Maybe:

data Maybe a = Some a | None

It's a type with

  • a param,
  • two constructors,
    • one of them takes a value of that type param

In a dependent type theory you might see a definition like that, but it’d be syntactic sugar for the more explicit:

Inductive Maybe (a : Type) : Type :=
| Some : a -> Maybe a
| None : Maybe a

Where we instead

  • give type signatures for each constructor explicitly,
    • which each need to end in returning the type we’re defining.

Actually in the end the type signatures of the constructors will also include the type parameter:

Some : forall (a : Type), a -> Maybe a

4.2.2. List type

Getting a little trickier, we could also translate a list type:

data List a = nil | cons a (List a)

desugars to

Inductive List (a : Type) : Type :=
| nil : List a
| cons : a -> List a -> List a

Both of these are polymorphic types, or more formally they’re polymorphic type families.

  • A group of types related by a common type former and common

polymorphic constructors.

4.2.3. Vector (Length-indexed list type)

The next step beyond a List type? A length-indexed list type. This I can’t express in ADT notation:

Inductive Vector (a : Type) : nat -> Type :=
| Vnil : Vector a 0
| Vcons : forall (n : nat), a -> Vector a n -> Vector a (1 + n)

Compare this with the definition of List and a few things stand out:

  • the signature of the type former has gone from Type to nat -> Type,
  • cons has an extra parameter for a length, and
  • the constructors no longer have exactly the same return type.

Trying to understand the syntax (this isn't valid, it's just to help me group things logically):

  • (Vector int): nat -> Type
  • Vector : Type -> nat -> Type

4.2.4. Type index

So, the first change: the type former now describes a family not just parameterized by the element type but also by the length.

However, by placing that nat after the :, it’s not scoped over the rest of the constructors as well. (a is)

This is called a type index, and what it means is each of our constructors can now involve multiple members of the type family at once and even determine which one they return.

  • Vnil shows that: it specifically returns a value of the 0-length vector types (one for each possible element type).
  • Vcons gets even more interesting:
    • like cons it takes a value and a sublist (well, sub-vector).
    • But now it has to be polymorphic in the length of that sub-vector, hence the extra parameter.
    • and it uses an expression to decide its return type:
      • it produces a value of the vector type one longer than its sub-vector.

So,

  • Vnil always produces vectors in the 0 length types, and
  • Vcons (essentially) increments the type index of its sub-vector at the same time as it collects an additional element.

Those two properties actually enforce the type index being the vector length, in a way that the compiler will keep track of for you.

Skipped a message about the type for a length-preserving map

4.2.5. Family of equal types

From these same building blocks we can make things like the family of equality types.

Values of which represent evidence that two expressions will evaluate to the same thing at runtime,

and type families that correspond to inductively defined logical props and relations. I’ll stop here for now tho. 😅

It has a rather nauseating definition at first glance, imo.

Inductive eq (A : Type) (a : A) : A -> Type :=
| eq_refl : eq A a a.

It’s a family of types, where the only inhabited ones correspond to reflexive equalities.

And then there’s usually some notation shenanigans so we can use = instead 😅

Reserved Notation "_ = _" (at level 70, no associativity).
Inductive eq {A : Type} (a : A) : A -> Type :=
| eq_refl : a = a
where "a = b" := eq a b.

The {} mean that parameter should be inferred automatically, because it’s constrained/redundant by the types of the later parameters. You only ever use eq with values, so A can always be inferred as the type of those values.

So, you can only construct things like eq_refl 5 : 5 = 5. Where that gets useful is the really funky way that type indexes interact with pattern matching- this is “dependent pattern matching”.

4.2.6. TODO Dependent pattern matching

If you have some H : a = b (constructed… somehow, I can get to that 😅) and pattern match on it the only possible branch will be eqrefl a. Except, the type of eqrefl a is a = a. What that means is, on that branch you “learn” (in a way the type checker can understand) that actually b must have been a all along. A similar thing happens if you pattern match on a vector of some generic length- on the Vnil branch you “learn” that that length was actually 0 all along, and on the Vcons branch you learn that the length is actually 1 + some smaller length. That substitution impacts the types of other values you have, refining your understanding of them. For example, say if you have two vectors that share the same length and pattern match on one of them. V1 V2 : Vector A n and you pattern match on V1. Well, on the Vnil branch you learn that n is 0. That also means that on this branch V2’s type is really Vector A 0. Similarly, on the Vcons branch you learn that n is really 1 + m (for some m that you got from the pattern matching). Well, now V2’s type can be refined to Vector A (1 + m) too.

Returning to eq, if you have a V : Vector A n and an H : 5 = n you can actually pattern match on H. There will be exactly one branch, but on that branch you’ll learn that n must really have been 5 all along! And so, you get to refine the type of V to Vector A 5. This all comes down to how the individual constructors get to specify the values of the type indexes they return: when you pattern match and end up on the branch corresponding to that constructor you also learn that actually whatever you had for that index must be interchangeable with whatever that constructor puts there. Otherwise you wouldn’t have made it to that branch, that constructor would not have been usable to construct that value. You can’t construct a Vnil whose length is non-0, and you can’t construct a Vcons whose length is 0. And you can’t construct an eqrefl where a and b are different.

When you pattern match on a value of an indexed type you get to do substitutions on the expression for the index in the types of other stuff.

In V1 V2 : Vector A n, n is an index. So, if you pattern match on V1 you get to substitute n with other stuff in any other types where it appears (like the type of V2)

The thing you get to substitute it with? Well, on each branch the constructor will give an expression of its own for the index. You get to substitute n with that. With eq, the LHS is a parameter and the RHS is an index.

So when you have H : 5 = n, 5 is the param and n is the index.

When you pattern match on it you’ll have exactly one branch where you reveal it must have been constructed by eqrefl and where the index is given as whatever the parameter was.

So you get to substitute n with 5 in any other types.

That’s why, btw, I specifically wrote it 5 = n instead of the other way around. You can write a symmetry helper, but I didn’t want to introduce that yet lol The “primitive” operation is specifically replacing whatever’s on the right side with whatever’s on the left side.

I also did a similar bit of slight of hand using 1 + n in the definition of Vector, for technical reasons that will probably make your eyes glaze over at this stage 😅

5. Backlinks

Created: 2025-03-20 Thu 01:16