Церковные списки в Haskell
Мне пришлось реализовать функцию map haskell для работы с церковными списками, которые определены следующим образом:
type Churchlist t u = (t->u->u)->u->u
В лямбда-исчислении списки кодируются следующим образом:
[] := λc. λn. n
[1,2,3] := λc. λn. c 1 (c 2 (c 3 n))
Образец решения этого упражнения:
mapChurch :: (t->s) -> (Churchlist t u) -> (Churchlist s u)
mapChurch f l = \c n -> l (c.f) n
У меня нет идеи, как это решение работает, и я не знаю, как создать такую функцию. У меня уже есть опыт исчисления лямбды и церковных цифр, но это упражнение было большой головной болью для меня, и я должен уметь понимать и решать такие проблемы для своего экзамена на следующей неделе. Может кто-то, пожалуйста, дайте мне хороший источник, где я мог бы научиться решать такие проблемы или дать мне небольшое руководство о том, как это работает?
Ответы
Ответ 1
Все структуры данных лямбда-исчисления являются, ну, функциями, потому что все это есть в лямбда-исчислении. Это означает, что представление для булевых, кортежей, списка, числа или чего-либо еще должно быть некоторой функцией, которая представляет собой активное поведение этой вещи.
Для списков это "складка". Неизменяемые односвязные списки обычно определяются List a = Cons a (List a) | Nil
, что означает, что единственными способами, которыми вы можете построить список, является либо Nil
, либо Cons anElement anotherList
. Если вы напишете его в стиле lisp, где c
есть Cons
и n
is Nil
, тогда список [1,2,3]
выглядит следующим образом:
(c 1 (c 2 (c 3 n)))
Когда вы выполняете сгиб над списком, вы просто предоставляете свои собственные "Cons
" и "Nil
" для замены списка. В Haskell библиотечная функция для этого - foldr
foldr :: (a -> b -> b) -> b -> [a] -> b
Посмотрите знакомый? Выньте [a]
, и у вас есть тот же тип, что и Churchlist a b
. Как я уже сказал, церковная кодировка представляет списки как функцию их сгибания.
Итак, пример определяет map
. Обратите внимание, как l
используется как функция: это функция, которая в любом случае сгибается над некоторым списком. \c n -> l (c.f) n
в основном говорит: "Замените каждый c
на c . f
и каждый n
на 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))
Теперь должно быть очевидно, что это действительно функция сопоставления, потому что она выглядит так же, как и исходная, за исключением 1
, превращенная в (f 1)
, 2
в (f 2)
и 3
в (f 3)
.
Ответ 2
Итак, давайте начнем с кодирования двух конструкторов списков, используя ваш пример в качестве ссылки:
[] := λc. λn. n
[1,2,3] := λc. λn. c 1 (c 2 (c 3 n))
[]
- это конец конструктора списка, и мы можем поднять это прямо из примера. []
уже имеет смысл в haskell, поэтому позвоните нам nil
:
nil = \c n -> n
Другим конструктором, который нам нужен, является элемент и существующий список и создается новый список. Канонически это называется cons
, с определением:
cons x xs = \c n -> c x (xs c n)
Мы можем проверить, что это соответствует приведенному выше примеру, поскольку
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)) =
Теперь рассмотрим цель функции map - применить данную функцию к каждому элементу списка. Поэтому давайте посмотрим, как это работает для каждого из конструкторов.
nil
не имеет элементов, поэтому mapChurch f nil
должен быть просто nil
:
mapChurch f nil
= \c n -> nil (c.f) n
= \c n -> (\c' n' -> n') (c.f) n
= \c n -> n
= nil
cons
имеет элемент и остальную часть списка, поэтому для того, чтобы mapChurch f
работал правильно, он должен применить f
к элементу и mapChurch f
к остальной части списка. То есть, mapChurch f (cons x xs)
должен быть таким же, как 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)
Итак, поскольку все списки сделаны из этих двух конструкторов, а mapChurch
работает на обоих из них, как ожидалось, mapChurch
должен работать как ожидалось во всех списках.
Ответ 3
Хорошо, мы можем прокомментировать тип Churchlist таким образом, чтобы его прояснить:
-- 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
Обратите внимание, что это тесно связано с функцией foldr
:
foldr :: (t -> u -> u) -> u -> [t] -> u
foldr k z [] = z
foldr k z (x:xs) = k x (foldr k z xs)
foldr
- очень общая функция, которая может реализовать всевозможные другие функции списка. Тривиальный пример, который поможет вам, - создать копию списка с помощью foldr
:
copyList :: [t] -> [t]
copyList xs = foldr (:) [] xs
Используя указанный выше тип комментария, foldr (:) []
означает следующее: "если вы видите пустой список, возвращайте пустой список и если вы видите пару return head:tailResult
."
Используя Churchlist
, вы можете легко написать аналогию следующим образом:
-- 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
Теперь, чтобы реализовать map
, вам просто нужно заменить cons
на подходящую функцию, например:
map :: (a -> b) -> [a] -> [b]
map f xs = foldr (\x xs' -> f x:xs') [] xs
Отображение похоже на копирование списка, за исключением того, что вместо простого копирования элементов дословно вы применяете f
к каждому из них.
Изучите все это внимательно, и вы сможете написать mapChurchlist :: (t -> t') -> Churchlist t u -> Churchlist t' u
самостоятельно.
Дополнительное упражнение (простое): напишите эти функции списка в терминах foldr
и напишите копии для 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
Если вам хочется заняться чем-то сложнее, попробуйте написать tail
для Churchlist
. (Начните с записи tail
для [a]
с помощью foldr
.)