I'm a newcomer to Haskell, so apologies if this question doesn't make too much sense.
I want to be able to implement simply typed lambda expressions in Haskell in such a way that when I try to apply an expression to another of the wrong type, the result is not a type error, but rather some set value, e.g. Nothing. At first I thought using the Maybe monad would be the right approach, but I've not been able to get anything working. I wondered what, if any, would be the correct way to do this.
The context of the problem, if it helps, is a project I'm working on which assigns POS (part of speech) tags to words in sentences. For my tag set, I'm using Categorial grammar types; these are typed lambda expressions like (e -> s)
or (e -> (e -> s))
, where e
and s
are the types for nouns and sentences respectively. So for example, kill
has the type (e -> (e -> s))
- it takes two noun phrases and returns a sentence. I want a write a function which takes a list of objects of such types, and finds out whether there is any way to combine them to reach an object of type s
. Of course, this is just what Haskell's type checker does anyway, so it should be simple to assign each word a lambda expression of the appropriate type, and let Haskell do the rest. The problem is that, if s
can't be reached, Haskell's type checker naturally stops the program from running.
Pretty standard stuff. Just write a type-checker, and only evaluate the term when it typechecks.
evalMay
does this. You can of course enrich the set of constants and base types; I just used one of each for simplicity.I'd like to extend @Daniel Wagner's excellent answer with a slightly different approach: instead of typechecking returning a valid type (if there is one), return a typed expression that is then guaranteed we can evaluate it (since the simply-typed lambda calculus is strongly normalizing). The basic idea is that
check ctx t e
returnsJust (ctx |- e :: t)
iffe
can be typed att
in some contextctx
, and then given some typed expressionctx |- e :: t
, we can evaluate it in someEnv
ironment of the right type.The implementation
I will be using singletons to emulate the Pi type of
check :: (ctx :: [Type]) -> (a :: Type) -> Term -> Maybe (TTerm ctx a)
, which means we will need to turn on every GHC extension and the kitchen sink:The first bit is the untyped representation, straight from @Daniel Wagner's answer:
but we will also give semantics for these types by interpreting
Base
as()
andArrow t1 t2
ast1 -> t2
:To keep with the de Bruijn theme, contexts are the list of types, and variables are indices of the context. Given an environment of a context type, we can look up a variable index to get a value. Note that
lookupVar
is a total function.OK we have all the infrastructure in place to actually write some code. First of all, let's define a typed representation of
Term
, together with a (total!) evaluator:So far so good.
eval
is nice & total because its input can only represent well-typed terms of the simply-typed lambda calculus. So part of the work from @Daniel's evaluator will have to be done in the transformation of the untyped representation to the typed one.The basic idea behind
infer
is that if type inference succeeds, it returnsJust $ TheTerm t e
, wheret
is aSing
leton witness ofe
's type.Hopefully the last step is obvious: since we can only evaluate a well-typed term at a given type (since that's what gives us the type of its Haskell embedding), we turn type
infer
ence into typecheck
ing:Example session
Let's try our functions out in GHCi: