I’ve been learning and using Haskell on-and-off for the past couple of years. One of my early complaints was that I couldn’t find a good library for working with variables and binding that used locally nameless terms. Recently though, I found unbound-generics
, which checks all my previously unfilled boxes.
Abstract binding trees (or ABTs) are abstract syntax trees (ASTs) augmented with the ability to capture the binding structure of a program. ABTs are one of the first topics we cover in 15-312 Principles of Programming Languages because variables show up in every interesting feature of a programming language.
I recently wrote at length about the various strategies for dealing with variables and binding and their implementations. While it’s a good exerciseIn fact, it’s hw1 for 15-312! If you’re curious, check out the handout.
to implement ABTs from scratch, in most cases I’d rather just use a library. In school we used abbot
, which is an ABT library for Standard ML. For tinkering with Haskell, I recently found unbound-generics
, which provides a similar API.
I gave it a test drive while learning how to implement type inference for the simply-typed lambda calculus (STLC) and was rather pleased. The source code for my STLC inference program is on GitHub if you’re looking for an example of unbound-generics
in action.
To pluck a few snippets out, here’s the definition of STLC terms:
data Term
= Tvar Tvar
| Tlam (Bind Tvar Term)
| Tapp Term Term
| Tlet Term (Bind Tvar Term)
| Tz
| Ts Term
| Tifz Term Term (Bind Tvar Term)
| Tbool Bool
| Tif Term Term Term
deriving (Show, Generic, Typeable)
Bind
is the abstract type for locally nameless terms that bind a variable. It’s cool in Haskell (compared to SML) because the compiler can automatically derive the locally nameless representation from this data type definition (with help from the unbound-generics
library).
Here’s what it looks like in use:
-- (This is a snippet from the type inference code)
Tlam bnd) = do
constraintsWithCon ctx (-- 'out' the ABT to get a fresh variable
-- (x used to be "locally nameless", but now has a globally unique name)
<- unbind bnd
(x, e) -- Generate fresh type variable to put into the context
<- Cvar <$> fresh (string2name "t1_")
t1 let ctx' = Map.insert x t1 ctx
<- constraintsWithCon ctx' e
t2 return $ Carrow t1 t2
Apart from out
being called unbind
and into
being called bind
, the API is pretty similar. Also, unlike abbot
, which required a standalone build step to generate SML code, unbound-generics
uses the Haskell’s derive Generic
to bake the code generation for capture avoiding substitution and alpha equivalence right into the compiler. All in all, unbound-generics
is really pleasant to use!