I had to implement the haskell map function to work with church lists which are defined as following:
type Churchlist t u = (t->u->u)->u->u
In lambda calculus, lists are encoded as following:
[] := λc. λn. n
[1,2,3] := λc. λn. c 1 (c 2 (c 3 n))
The sample solution of this exercise is:
mapChurch :: (t->s) -> (Churchlist t u) -> (Churchlist s u)
mapChurch f l = \c n -> l (c.f) n
I have NO idea how this solution works and I don't know how to create such a function. I have already experience with lambda calculus and church numerals, but this exercise has been a big headache for me and I have to be able to understand and solve such problems for my exam next week. Could someone please give me a good source where I could learn to solve such problems or give me a little guidance on how it works?
All lambda calculus data structures are, well, functions, because that's all there is in the lambda calculus. That means that the representation for a boolean, tuple, list, number, or anything, has to be some function that represents the active behavior of that thing.
For lists, it is a "fold". Immutable singly-linked lists are usually defined List a = Cons a (List a) | Nil
, meaning the only ways you can construct a list is either Nil
or Cons anElement anotherList
. If you write it out in lisp-style, where c
is Cons
and n
is Nil
, then the list [1,2,3]
looks like this:
(c 1 (c 2 (c 3 n)))
When you perform a fold over a list, you simply provide your own "Cons
" and "Nil
" to replace the list ones. In Haskell, the library function for this is foldr
foldr :: (a -> b -> b) -> b -> [a] -> b
Look familiar? Take out the [a]
and you have the exact same type as Churchlist a b
. Like I said, church encoding represents lists as their folding function.
So the example defines map
. Notice how l
is used as a function: it is the function that folds over some list, after all. \c n -> l (c.f) n
basically says "replace every c
with c . f
and every n
with n
".
(c 1 (c 2 (c 3 n)))
-- replace `c` with `(c . f)`, and `n` with `n`
((c . f) 1 ((c . f) 2) ((c . f) 3 n)))
-- simplify `(foo . bar) baz` to `foo (bar baz)`
(c (f 1) (c (f 2) (c (f 3) n))
It should be apparent now that this is indeed a mapping function, because it looks just like the original, except 1
turned into (f 1)
, 2
to (f 2)
, and 3
to (f 3)
.
So let's start by encoding the two list constructors, using your example as reference:
[] := λc. λn. n
[1,2,3] := λc. λn. c 1 (c 2 (c 3 n))
[]
is the end of list constructor, and we can lift that straight from the example. []
already has meaning in haskell, so let's call ours nil
:
nil = \c n -> n
The other constructor we need takes an element and an existing list, and creates a new list. Canonically, this is called cons
, with the definition:
cons x xs = \c n -> c x (xs c n)
We can check that this is consistent with the example above, since
cons 1 (cons 2 (cons 3 nil))) =
cons 1 (cons 2 (cons 3 (\c n -> n)) =
cons 1 (cons 2 (\c n -> c 3 ((\c' n' -> n') c n))) =
cons 1 (cons 2 (\c n -> c 3 n)) =
cons 1 (\c n -> c 2 ((\c' n' -> c' 3 n') c n) ) =
cons 1 (\c n -> c 2 (c 3 n)) =
\c n -> c 1 ((\c' n' -> c' 2 (c' 3 n')) c n) =
\c n -> c 1 (c 2 (c 3 n)) =
Now, consider the purpose of the map function - it is to apply the given function to each element of the list. So let's see how that works for each of the constructors.
nil
has no elements, so mapChurch f nil
should just be nil
:
mapChurch f nil
= \c n -> nil (c.f) n
= \c n -> (\c' n' -> n') (c.f) n
= \c n -> n
= nil
cons
has an element and a rest of list, so, in order for mapChurch f
to work propery, it must apply f
to the element and mapChurch f
to rest of the list. That is, mapChurch f (cons x xs)
should be the same as cons (f x) (mapChurch f xs)
.
mapChurch f (cons x xs)
= \c n -> (cons x xs) (c.f) n
= \c n -> (\c' n' -> c' x (xs c' n')) (c.f) n
= \c n -> (c.f) x (xs (c.f) n)
-- (c.f) x = c (f x) by definition of (.)
= \c n -> c (f x) (xs (c.f) n)
= \c n -> c (f x) ((\c' n' -> xs (c'.f) n') c n)
= \c n -> c (f x) ((mapChurch f xs) c n)
= cons (f x) (mapChurch f xs)
So since all lists are made from those two constructors, and mapChurch
works on both of them as expected, mapChurch
must work as expected on all lists.
Well, we can comment the Churchlist type this way to clarify it:
-- Tell me...
type Churchlist t u = (t -> u -> u) -- ...how to handle a pair
-> u -- ...and how to handle an empty list
-> u -- ...and then I'll transform a list into
-- the type you want
Note that this is intimately related to the foldr
function:
foldr :: (t -> u -> u) -> u -> [t] -> u
foldr k z [] = z
foldr k z (x:xs) = k x (foldr k z xs)
foldr
is a very general function that can implement all sorts of other list functions. A trivial example that will help you is implementing a list copy with foldr
:
copyList :: [t] -> [t]
copyList xs = foldr (:) [] xs
Using the commented type above, foldr (:) []
means this: "if you see an empty list return the empty list, and if you see a pair return head:tailResult
."
Using Churchlist
, you can easily write the counterpart this way:
-- Note that the definitions of nil and cons mirror the two foldr equations!
nil :: Churchlist t u
nil = \k z -> z
cons :: t -> Churchlist t u -> Churchlist t u
cons x xs = \k z -> k x (xs k z)
copyChurchlist :: ChurchList t u -> Churchlist t u
copyChurchlist xs = xs cons nil
Now, to implement map
, you just need to replace cons
with a suitable function, like this:
map :: (a -> b) -> [a] -> [b]
map f xs = foldr (\x xs' -> f x:xs') [] xs
Mapping is like copying a list, except that instead of just copying the elements verbatim you apply f
to each of them.
Study all of this carefully, and you should be able to write mapChurchlist :: (t -> t') -> Churchlist t u -> Churchlist t' u
on your own.
Extra exercise (easy): write these list functions in terms of foldr
, and write counterparts for Churchlist
:
filter :: (a -> Bool) -> [a] -> [a]
append :: [a] -> [a] -> [a]
-- Return first element of list that satisfies predicate, or Nothing
find :: (a -> Bool) -> [a] -> Maybe a
If you're feeling like tackling something harder, try writing tail
for Churchlist
. (Start by writing tail
for [a]
using foldr
.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With