Solving Constraints Generically

by Phil Freeman on 2012/02/06

In this post, I'd like to revisit Algorithm W, which I discussed when I wrote about Purity's typechecker.

Recalling the approach taken before, a term was typed by collecting constraints between unknown type variables by traversing the term in question, and then solving those constraints by substitution. This time I'd like to generalize the second part of the algorithm, to solve constraints over any type functor by substitution.

Then a constraint for a type functor f asserts equality between an unknown (identified by an integer) and an incomplete f-tree with variables in Unknown. It is represented as a pair:

type Constraint f = (Unknown, Free f Unknown)

A solution set is just a mapping from unknowns to incomplete f-trees:

type SolutionSet f = Unknown -> Free f Unknown

A functor is Unifiable if it defines types which can be unified to give constraints. It is expected that unknowns unify with all terms. For simplicity, I have used error below to indicate the failure of terms to unify, but one could rewrite unify to give values in some monad supporting errors.

solve takes a set of constraints and returns a solution set. It does so by repeatedly substituting the first constraint into the remaining constraints, and into the solution set, removing one unknown at a time. As it does so, it may be necessary to unify one or more types, generating further constraints. These are added to the queue of constraints still to be solved.

One can check that solve does indeed determine the correct type for the S combinator:

ghci> solve example 8

((2 -> (4 -> 5)) -> ((2 -> 4) -> (2 -> 5)))

Now that we can solve constraints, let's write a method to determine constraints for the specific example of terms of the simply-typed lambda calculus. There are three types of terms: variables, applications and abstractions. To save passing around environments containing strings, we will represent the bound variables in a term using a type variable. Abstraction will introduce a new type variable, and parametricity will prevent us from creating any ill-defined terms:

Now that we have all of the required pieces, we can implement generateConstraints, which takes a term and generates a set of constraints. We can use the method above to solve for the types of all subterms. The method works by assigning names to all subterms in a bottom-up fashion, and adding constraints between those names where necessary.