Intuition of Church Encoding
After first being introduced to the concept of Church numerals in Structure and Interpretation of Computer Programs, I decided to learn more about them and the more general topic, Church encoding. For those who don’t know, Church encoding defines a way to represent common data types and structures in the Lambda calculus using only functions, since functions are the only terms available.
I eventually stumbled upon a pretty neat intuition for how Church encodings work, which enables you to create encodings for arbitrary data types very easily. But first, a little on how I got there…
I started with the Wikipedia entry, which has a decent amount of examples but is lacking in providing any real intuition into how they work. As an example, the Church encoding for booleans are listed as follows.
true = λa. λb. a
false = λa. λb. b
These two functions are the simplest possible two distinct terms,
ignoring the identity function so that there can at least be some
symmetry (same number of arguments), so in that sense it may seem
intuitive to use them. And I can accept that it’s convention in the
Lambda calculus to use single letter identifiers, but still I can’t
help but wonder what more meaningful identifiers I would use—
The Church encoding for natural numbers is a little better.
zero = λf. λx. x
succ = λn. λf. λx. f (n f x)
The common intuition for this encoding is that the encoded
n
represents a function that takes another function
f
as an argument and returns a new function which
applies f
to its argument n
times. This is
backed up by observing that zero
takes any function
f
and simply returns the identity function—f
zero times does nothing. That’s more satisfying,
but I do wonder if it’s a coincidence that zero is alpha equivalent
to false.
Next up, lists—
nil = λc. λn. n
cons = λh. λt. λc. λn. c h (t c n)
The intuition is that a list is encoded as a fold function, although it might be difficult to see from these
terms. And now I’m really starting to doubt that it’s mere
coincidence that yet again we have an alpha equivalence with
false
, this time in the encoding of nil
.
Implementation in Haskell
It was about this time that I decided to try implementing them in Haskell. There are a few implementations out there on the internet already but I wanted to try it from scratch. To start with, I figured I’d just bash in the definitions without type signatures and see what I get.
Church booleans
λ let true' = \a b -> a λ :t true' true' :: t1 -> t -> t1 λ let false' = \a b -> b λ :t false' false' :: t -> t1 -> t1
Defining a type that works in all the ways I would like is a little trickier than I expected, but not too difficult.
{-# LANGUAGE RankNTypes #-} newtype Bool' = Bool' { runBool' :: forall a. a -> a -> a } true' :: Bool' true' = Bool' $ \a b -> a false' :: Bool' false' = Bool' $ \a b -> b
Church numerals
λ let zero' = \f x -> x λ :t zero' zero' :: t -> t1 -> t1 λ let succ' = \n f x -> f (n f x) λ :t succ' succ' :: ((t1 -> t) -> t2 -> t1) -> (t1 -> t) -> t2 -> t
The trick for this one is in recognising that the first argument to
succ’
is supposed to be a numeral, so its type signature
in succ’
should give us a good place to start.
newtype Nat' = Nat' { runNat' :: forall a. (a -> a) -> a -> a } zero' :: Nat' zero' = Nat' $ \f x -> x succ' :: Nat' -> Nat' succ' n = Nat' $ \f x -> f (runNat' n f x)
Church lists
λ let nil' = \c n -> n λ :t nil' nil' :: t -> t1 -> t1 λ let cons' = \h t c n -> c h (t c n) λ :t cons' cons' :: t1 -> ((t1 -> t2 -> t) -> t3 -> t2) -> (t1 -> t2 -> t) -> t3 -> t
Again, the trick here was in recognising that the return type after two arguments is a list.
newtype List' a = List' { runList' :: forall b. (a -> b -> b) -> b -> b } nil' :: List' a nil' = List' $ \c n -> n cons' :: a -> List' a -> List' a cons x xs = List' $ \c n -> c x (runList' xs c n)
Comparison with standard definitions
At this point I thought it would be interesting to compare these types with the standard type definitions in the Prelude for the actual types. I decided to list out the standard data definitions in generalised algebraic datatype format.
data Bool = True :: Bool | False :: Bool newtype Bool' = Bool' { runBool' :: forall a. a -> a -> a } data Nat = Succ :: Nat -> Nat | Zero :: Nat newtype Nat' = Nat' { runNat' :: forall a. (a -> a) -> a -> a } data List a = Cons :: a -> List a -> List a | Nil :: List a newtype List' a = List' { runList' :: forall b. (a -> b -> b) -> b -> b }
And as I did this, I noticed a pattern! If you take the type
signature of the run
function and replace the quantified
type with the type being declared, you get something very
interesting.
Bool' -> Bool' -> Bool' (Nat' -> Nat') -> Nat' -> Nat' (a -> List' a -> List' a) -> List' a -> List' a
In all three cases, the final term is the type itself, and the other terms are the types of the data constructors! Go back up and check for yourself.
The revelation
What does this mean? It means that the Church encodings are simply abstractions over data constructors! The arguments to the functions of the Church encodings should be data constructors! Let’s go back and try it for ourself.
λ runBool' true' True False True λ runBool' false' True False False λ runNat' zero' (+1) 0 0 λ runNat' (succ' (succ' zero')) (+1) 0 2 λ runList' nil' (:) [] [] λ runList' (cons' 1 (cons' 2 nil')) (:) [] [1,2]
And just like all good revelations, now that I see it it seems so obvious.
So with this knowledge in hand, we can go on to define Church
encodings for any data type we can think of. Let’s try Haskell’s
Maybe
and Either
types, and while we’re at
it we can use more meaningful identifiers for the lambda terms.
data Maybe a = Just :: a -> Maybe a | Nothing :: Maybe a newtype Maybe' a = Maybe' { runMaybe' :: forall b. (a -> b) -> b -> b } nothing' :: Maybe' a nothing' = Maybe' $ \just nothing -> nothing just' :: a -> Maybe' a just' a = Maybe' $ \just nothing -> just a data Either a b = Left :: a -> Either a b | Right :: b -> Either a b newtype Either' a b = Either' { runEither' :: forall c. (a -> c) -> (b -> c) -> c } left' :: a -> Either' a b left' a = Either' $ \left right -> left a right' :: b -> Either' a b right' b = Either' $ \left right -> right b
You can find these type definitions and related function definitions in my GitHub repository.