How to keep lambda calculus simple
hirrolot.github.io
hirrolot.github.io
The lambda calculus is simple. It consists of only four essential things, and with those four things we can compute anything that is computable. (I don't claim that the lambda calculus is simple to understand — certainly there are people who have a hard time grasping it. But that's a different issue.)
What the author is doing is not "the lambda calculus"; it's an extended lambda calculus for dependent types. That's a huge step in terms of complexity. Yes, it might be the simplest lambda calculus for the task (I didn't check), but that is not at all "how to keep lambda calculus simple". Just a very odd choice for a title, in my opinion.
In the Idris2 github repository, Guillaume Allais goes a step further and shows a well-named version. There the types of terms and values are indexed by the list of names in the environment and the compiler checks that the manipulation of deBruin levels and indices is correct:
https://github.com/idris-lang/Idris2/blob/main/libs/papers/L...
I love functional programming and have had some fun diving deep into some of the underlying category theory concepts at times, but I feel like 95% of the time trying to introduce these advanced concepts into a practical, professional codebase will lead to severe headache.
It also doesn't mention any category theory...
Perhaps I'm slightly misinterpreting the usage of "simple" here and it's related to the more technical notion of simply typed lambda calculus.
I would have found this blog post helpful when I was implementing the original tutorial paper, because I got stuck with the higher-order abstract syntax for lambdas. The original uses lambda literals from Haskell, which I couldn't figure it out how to express in Coq or Rust, because of the type recursion and some other issues. Whereas, I would easily be able to implement the first-order alternative presented here in either language.
> Church’s type theory is a formulation of type theory that was introduced by Alonzo Church in Church 1940. In certain respects, it is simpler and more general than the type theory introduced by Bertrand Russell in Russell 1908 and Whitehead & Russell 1927a. Since properties and relations can be regarded as functions from entities to truth values, the concept of a function is taken as primitive in Church’s type theory, and the λ-notation which Church introduced in Church 1932 and Church 1941 is incorporated into the formal language.
We distinguish two kinds of variables: Bound variables and
Free ones. The first kind of variables is called De Bruijn
indices: each bound variable is a natural number (starting
from zero) that designates the number of binders between
itself and a corresponding binder. For example, the term \x -
> \y -> x (the “K combinator”) is represented as \_ -> \_ -> 1.
(If we were returning y instead of x, we would write 0
instead.)
The footnote says: Contrary to a De Bruijn index, a De Bruijn level is a natural
number indicating the number of binders between the
variable’s binder and the term’s root. For example, the term
\x -> \y -> x is represented as \_ -> \_ -> 0, where 0 is the
De Bruijn level of x. The De Bruijn level of y would be 1 if
we used it instead of x.↩If you assign integers from the inside out, as DB indices do, then the `x` usage in `\x -> \y -> x` passes one binder (`\y`) before landing on the correct one (`\x`), yielding `\_ -> \_ -> 1`.
If you assign integers from the outside in, as DB levels do, then the `x` usage lands on `\x` instantly, yielding `\_ -> \_ -> 0`.
De Bruijn came up with both systems, and it can be confusing to keep track of which one is which; but they are different systems.
because one is index and the other is a level but notationally they refer to the same syntactic construct, and therefore an assertion of value differs by intent. Index 1, level 0, Same syntax to represent.
To me, it "looks" contradictory. It has to be explained by word not by statement in notation.
I'm sure the author is well aware, but Lennart Augustsson wrote a really nice blog post responding to the original "Simply Easy!" in 2007 that was a lot more fun and simple than the "Simply Easy" paper.
http://augustss.blogspot.com/2007/10/simpler-easier-in-recen...
data ITerm
= Ann CTerm Type
| Bound Int
| Free Name
| App ITerm CTerm
deriving (Show, Eq) t ::= x (variable)
| \x:T,t (abstraction)
| t t (application)
| true (constant true)
| false (constant false)
| if t then t else t (conditional)
Was simpler. true ::= \x\y,x
false ::= \x\y,y
Then the conditional comes for free, since it is an application of true. You can define not as not ::= \b, b false true
You can then define 'and' as and ::= \x\y, x y falseIs this correct?
Structurally, if `true := \x\y.x`, the simplest attempt at typing it would be `true : bool -> bool -> bool`. But that means `true` can't have type `bool`!
What you need is to say that true (like false) chooses one of its two inputs to return, no matter what type that happens to be. So we introduce a type parameter (think Java generics, if that's what you're familiar with).
"Given any type T, `true T` has type T -> T -> T". A formalized notation for this (there is more than one notation you'll see) is `true : ∀ (T : Type), T -> T -> T`. And `false` has the same type. This type obviates the need for a declared `bool` type.
Your data structure is also not the simplest choice of syntax for lambda calculus, since you require an explicit type annotation on every function variable. I don't think a user of your implementation would find it "simple" to write even the identity function for a non-trivial type.
If you want a simple typechecker then you need type annotations. If you want to have type inference then you either need to implement unification or go the bidir route (which still requires top level annotations). You can't have it both ways.
You are right that the typechecker gets significantly more complicated if type annotations are optional. But the article and the paper are about real implementations of the language, not arm-chair ones where the burden of type annotations isn't important.
Here is an STLC with bidir example which I believe to be easier to understand: https://github.com/solomon-b/lambda-calculus-hs/blob/main/ma...
[1] https://gist.github.com/Hirrolot/27e6b02a051df333811a23b97c3...