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

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

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

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

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

s i i

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

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

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)

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

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

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


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

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.)

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

Ответ 1

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

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

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

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

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

data Value = V Int [Term]

Итак, Value представляет собой Int, представляющий конструктор значений ADT, за которым следует один 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"

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

Ответ 2

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

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.