λ-calculus

V. Списки

Основы списков

Давайте теперь реализуем какую-нибудь сложную структуру данных, например, список. Список, конечно, односвязный. Пусть мы хотим, чтобы list хранил в себе список из чисел 1, 2, 3. Тогда эту переменную мы определим таким образом, что выражение list f 0 будет равно f 1 (f 2 (f 3 0)). Говоря в терминах императивных языков программирования, list принимает два аргумента: функцию объединения и какой-то параметр, который мы назовем аккумулятором, а затем проходит по списку справа налево и объединяет очередной элемент с аккумулятором. Иначе говоря, производит правую свертку. Чем-то похож на цикл for с конца.

Список из трех элементов 1, 2, 3, например, реализуется так:

list123 := \f. \acc. f 1 (f 2 (f 3 acc))

Пустой список выглядит так (он, кстати, совпадает с false и zero, удивительное дело):

nil := \f. \acc. acc

Список из одного элемента конструируется так:

singleton := \x. \f. \acc. f x acc

Добавление элемента h в начало списка t (сокращения от слов head и tail). Никого же не смущает функция, принимающая четыре аргумента, да?

cons := \h. \t. \f. \acc. f h (t f acc)

Конкатенация (склеивание) списков:

concat := \alist. \blist. \f. \acc. alist f (blist f acc)

Забавно, но конкатенация списков также умеет складывать числа.

Проверка списка на пустоту похожа на проверку числа на нуль:

isnul := \list. list (\x. \acc. false) true

Подсчет длины списка:

length := \list. list (\x. \acc. succ acc) 0

Или, если применить η-преобразование:

length := \list. list (\x. succ) 0

Получение первого элемента списка (false, если список пустой):

head := \list. list (\x. \acc. x) false

Кстати, заметили опять знакомые выражения? head =b pair true false, даже удивительно.

Получение первого элемента списка со значением по умолчанию в случае пустого списка:

headdef := \def. \list. list (\x. \acc. x) def

А еще headdef =b pair true, если вы хотите кого-нибудь запутать.

Расслоение списков

Научимся теперь расслаивать список на пару (head, tail), то есть пару из первого элемента списка и списка без первого элемента (хвоста списка). Пожалуй, самое сложное из того, что нам приходилось создавать на текущий момент.

Первое, что приходит в голову — попробовать написать что-то наподобие:

split := \list. list (\x. \acc. pair x acc) (...)

То есть втупую на каждом шаге создавать пару из головы и хвоста.

Но всплывает две проблемы. Первая — acc не является списком. Это результат функции split от хвоста списка. То есть пара.

Чтобы превратить split от списка обратно в список, мы можем использовать cons:

unsplit := \splitres. cons (first splitres) (second splitres)

Так как пара вызывает переданную ей функцию от своих первого и второго элемента, это можно упростить до:

unsplit := \splitres. splitres cons

Наконец, новая версия split:

split := \list. list (\x. \acc. pair x (unsplit acc)) (...)

У нас осталась другая проблема. Не очень понятно, что должно быть на месте ..., чтобы все работало.

Но вот что должен выдать split от списка из одного элемента мы точно знаем, так что давайте попробуем вычислить split (singleton E):

singleton E -> \f. \acc. f E acc

split (singleton E) ->
-> (singleton E) (\x. \acc. pair x (unsplit acc)) (...) ->
-> (\x. \acc. pair x (unsplit acc)) E (...) ->
-> pair E (unsplit (...))

А должно быть pair E nil, то есть unsplit (...) -> (...) cons -> nil. Так что пусть ... принимает один аргумент и всегда возвращает nil.

split := \list. list (\x. \acc. pair x (unsplit acc)) (\x. nil)

Значит, теперь у нас split (singleton E) -> pair E nil, что нам и нужно, а дальше функция редукции \x. \acc. pair x (unsplit acc) будет работать как ожидалось.

Можно провести аналогию с таким циклом:

acc = \x. nil
for x in reversed list:
    tail = acc cons
    acc = pair x tail
return acc

Ну и, конечно, теперь можно реализовать функцию tail, которая возвращает список без своего первого элемента:

tail := \list. second (split list)

Интересно, кстати, что все же произойдет, если split запустить от пустого списка? Для пустого списка split nil -> \x. nil по определению, но что будет, если это выражение интерпретировать как пару?

first (\x. nil) -> (\x. nil) (\a. \b. a) -> nil
second (\x. nil) -> (\x. nil) (\a. \b. b) -> nil

Ожидаемо, tail nil -> nil. Не очень ожидаемо, что first (split nil) -> nil. Но тут стоит благодарить случай за то, что nil и false кодируются одинаково, и first (split nil) =b nil =b false =b head nil, так что какая-то консистентность сохранилась.