Приключения с нетипизированным лямбда-исчислением

Время от времени люди спрашивают о реализации нетипизированного лямбда-исчисления в Haskell. [Естественно, теперь я не могу найти ни один из этих вопросов, но я уверен, что видел их!] Просто ради смеха, я решил потратить некоторое время на игру с этим.

Это достаточно тривиально, чтобы сделать что-то вроде

i = \ x -> x
k = \ x y -> x
s = \ f g x -> (f x) (g x)

Это работает отлично. Однако, как только вы попытаетесь сделать что-то вроде

s i i

средство проверки типов справедливо жалуется на бесконечный тип. По сути, все в нетипизированном лямбда-исчислении — это функции, что по существу означает, что все функции имеют бесконечную арность. Но Haskell допускает только функции конечной арности. (Потому что, действительно, зачем вам бесконечная арность?)

Что ж, оказывается, мы можем легко обойти это ограничение:

data Term = T (Term -> Term)

T f ! x = f x

i = T $ \ x -> x
k = T $ \ x -> T $ \ y -> x
s = T $ \ f -> T $ \ g -> T $ \ x -> (f ! x) ! (g ! x)

Это прекрасно работает и позволяет создавать и выполнять произвольные лямбда-выражения. Например, мы можем легко создать функцию, которая превращает Int в число Чёрча:

zero = k ! i
succ = s ! (s ! (k ! s) ! k)

encode 0 = zero
encode n = succ ! (encode $ n-1)

Опять же, это работает отлично.

Теперь напишите функцию декодирования.

Да, удачи с этим! Проблема в том, что мы можем создавать произвольные лямбда-термы, но мы не можем их инспектировать каким-либо образом. Поэтому нам нужно добавить какой-то способ сделать это.


На данный момент лучшая идея, которую я придумал, заключается в следующем:

data Term x = F (Term x -> Term x) | R (Term x -> x)

F f ! x =            f x
R f ! x = R $ \ _ -> f x

out :: Term x -> x
out (R f) = f (error "mu")
out (F _) =   (error "nu")

i = F $ \ x -> x
k = F $ \ x -> F $ \ y -> x
s = F $ \ f -> F $ \ g -> F $ \ x -> (f ! x) ! (g ! x)

Теперь я могу сделать что-то вроде

decode :: Term Int -> Int
decode ti = out $ ti ! R (\ tx -> 1 + out tx) ! R (\ tx -> 0)

Это отлично работает для церковных булов и церковных цифр.


Дела начинают идти ужасно неправильно, когда я начинаю пытаться делать что-то высокоуровневое. Отбросив всю информацию о типах для реализации нетипизированного лямбда-исчисления, я теперь изо всех сил пытаюсь убедить средство проверки типов позволить мне делать то, что я хочу.

Это работает:

something = F $ \ x -> F $ \ n -> F $ \ s -> s ! x
nothing   =            F $ \ n -> F $ \ s -> n

encode :: Maybe x -> Term x
encode (Nothing) = nothing
encode (Just  x) = something ! x

Это не:

decode :: Term x -> Maybe (Term x)
decode tmx = out $ tmx ! R (\ tx -> Nothing) ! R (\ tx -> Just tx)

Я пробовал дюжину небольших вариаций этого; ни один из них не проверяет тип. Дело не в том, что я не понимаю, почему это терпит неудачу, а в том, что я не могу придумать, как добиться успеха. (В частности, R Just явно плохо набрано.)

Как будто мне нужна функция forall x y. Term x -> Term y. Потому что для чистых нетипизированных терминов это всегда должно быть возможно. Это не сработает только в терминах, связанных с R. Но я не могу понять, как сформулировать это в системе типов Haskell.

(Например, попробуйте изменить тип F на forall x. Term x -> Term x. Теперь определение k имеет неправильный тип, поскольку внутреннее F $ \ y -> x не может фактически возвращать любой тип, а только [теперь исправлено] тип x .)

У кого-нибудь умнее меня есть идея получше?


person MathematicalOrchid    schedule 10.07.2015    source источник


Ответы (2)


ОК, я нашел решение:

Код выше имеет Term x, параметризованный типом результата для R. Вместо того, чтобы делать это (и сводить с ума средство проверки типов), создайте некоторый тип Value, который может представлять любой тип результата, который вы когда-либо захотите вернуть. Теперь у нас есть

data Term = F (Term -> Term) | R (Term -> Value)

Свернув все возможные типы результатов в один непрозрачный тип Value, мы можем выполнять свою работу.

Конкретно, тип, который я выбрал,

data Value = V Int [Term]

Таким образом, Value — это Int, представляющий конструктор значений АТД, за которым следует один Term для каждого поля этого конструктора. С этим определением мы наконец можем сделать

decode :: Term -> Maybe Term
decode tmx =
  case tmx ! R (\ _ -> V 0 []) ! R (\ tx -> V 1 [tx]) of
    V 0 []   -> Nothing
    V 1 [tx] -> Just tx
    _        -> error "not a Maybe"

Точно так же вы можете кодировать и декодировать списки следующим образом:

null =                        F $ \ n -> F $ \ c -> n
cons = F $ \ x -> n $ \ xs -> F $ \ n -> F $ \ c -> c ! x ! xs

encode :: [Term] -> Term
encode (  []) = null
encode (x:xs) = cons ! x ! encode xs

decode :: Term -> [Term]
decode txs =
  case out $ txs ! R (\ txs -> V 0 []) ! F (\ tx -> R $ \ txs -> V 1 [tx, txs]) of
    V 0 []        -> []
    V 1 [tx, txs] -> tx : decode txs
    _             -> error "not a list"

Конечно, вы должны угадать, какие функции декодирования вам нужно применить. Но это нетипизированное лямбда-исчисление для вас!

person MathematicalOrchid    schedule 10.07.2015
comment
Я думаю, что data Term = Lam (Term -> Term) | App Term Term | Var Int было бы более простым решением. - person András Kovács; 11.07.2015
comment
@ Андрас Ковач, ты имел в виду Lam Term? - person user3237465; 11.07.2015
comment
@ user3237465 нет, я имел в виду Term -> Term. Мы можем сделать похожий ответ, как указано выше, но IMO чище. - person András Kovács; 11.07.2015
comment
@András Kovács, ах, смотри. Это очень похоже на quote из самого короткого бета-нормализатора (другие авторы называют эту функцию readback). - person user3237465; 11.07.2015

Это не ответ, но комментирование слишком ограничительно.

R Just плохо типизирован, потому что его тип рекурсивный, но мы всегда можем обернуть эту рекурсию на уровне типа в тип данных:

data Fix2 g f = Fix2 { run :: g (f (Fix2 g f)) }

Fix2 можно представить в терминах Fix и композиции конструкторов типов, но я не хочу все усложнять.

Тогда мы можем определить decode как

decode :: Term (Fix2 Maybe Term) -> Maybe (Term (Fix2 Maybe Term))
decode tmx = run $ out $ tmx ! R (Fix2 . const Nothing) ! R (Fix2 . Just)

Некоторые тесты:

isSomething :: Term (Fix2 Maybe Term) -> Bool
isSomething = isJust . decode

i = F id

main = do
    print $ isSomething (something ! i) -- True
    print $ isSomething  nothing        -- False

Но ясно, что Term (Fix2 Maybe Term) далеко не Term a.

person user3237465    schedule 11.07.2015