Проверка типов и рекурсивные типы (Написание Y-комбинатора в Haskell / Ocaml)

21

При объяснении Y-комбинатора в контексте Haskell обычно отмечается, что прямая реализация не будет проверять тип в Haskell из-за его рекурсивного типа.

Например, из Rosettacode :

The obvious definition of the Y combinator in Haskell canot be used
because it contains an infinite recursive type (a = a -> b). Defining
a data type (Mu) allows this recursion to be broken.

newtype Mu a = Roll { unroll :: Mu a -> a }

fix :: (a -> a) -> a
fix = \f -> (\x -> f (unroll x x)) $ Roll (\x -> f (unroll x x))

И действительно, «очевидное» определение не проверяет тип:

λ> let fix f g = (\x -> \a -> f (x x) a) (\x -> \a -> f (x x) a) g

<interactive>:10:33:
    Occurs check: cannot construct the infinite type:
      t2 = t2 -> t0 -> t1
    Expected type: t2 -> t0 -> t1
      Actual type: (t2 -> t0 -> t1) -> t0 -> t1
    In the first argument of `x', namely `x'
    In the first argument of `f', namely `(x x)'
    In the expression: f (x x) a

<interactive>:10:57:
    Occurs check: cannot construct the infinite type:
      t2 = t2 -> t0 -> t1
    In the first argument of `x', namely `x'
    In the first argument of `f', namely `(x x)'
    In the expression: f (x x) a
(0.01 secs, 1033328 bytes)

Такое же ограничение существует в Ocaml:

utop # let fix f g = (fun x a -> f (x x) a) (fun x a -> f (x x) a) g;;
Error: This expression has type 'a -> 'b but an expression was expected of type 'a                                    
       The type variable 'a occurs inside 'a -> 'b

Однако в Ocaml можно разрешить рекурсивные типы, передавая -rectypesпереключатель:

   -rectypes
          Allow  arbitrary  recursive  types  during type-checking.  By default, only recursive
          types where the recursion goes through an object type are supported.

Используя -rectypesвсе работает:

utop # let fix f g = (fun x a -> f (x x) a) (fun x a -> f (x x) a) g;;
val fix : (('a -> 'b) -> 'a -> 'b) -> 'a -> 'b = <fun>
utop # let fact_improver partial n = if n = 0 then 1 else n*partial (n-1);;
val fact_improver : (int -> int) -> int -> int = <fun>
utop # (fix fact_improver) 5;;
- : int = 120

Будучи любопытным по поводу систем типов и вывода типов, это вызывает некоторые вопросы, на которые я до сих пор не могу ответить.

  • Во-первых, как средство проверки типов может определить тип t2 = t2 -> t0 -> t1? Придумав этот тип, я думаю, проблема в том, что type ( t2) ссылается на себя с правой стороны?
  • Во-вторых, и, возможно, самое интересное, в чем причина того, что системы типа Haskell / Ocaml не допускают этого? Я думаю, что есть веская причина, так как Ocaml также не разрешит его по умолчанию, даже если он может работать с рекурсивными типами, если он -rectypesвключен.

Если это действительно большие темы, я был бы признателен за ссылки на соответствующую литературу.

бета
источник

Ответы:

16

Во-первых, ошибка GHC,

GHC пытается объединить несколько ограничений x, во-первых, мы используем его как функцию так

x :: a -> b

Далее мы используем его как значение для этой функции

x :: a

И, наконец, мы объединяем его с исходным выражением аргумента так

x :: (a -> b) -> c -> d

Теперь x xстановится попыткой объединить t2 -> t1 -> t0, однако, мы не можем объединить это, так как это потребует объединения t2, первый аргумент x, с x. Отсюда и наше сообщение об ошибке.

Далее, почему бы не общие рекурсивные типы. Что ж, первое, на что стоит обратить внимание, это разница между равными и изорекурсивными типами,

  • Экви-рекурсивное - это то, что вы ожидаете, mu X . Typeв точности эквивалентно его произвольному расширению или складыванию.
  • ISO-рекурсивные типы обеспечивают пару операторов, foldи unfoldкоторый складывать и раскладывать рекурсивные определения типов.

В настоящее время эквикурсивные типы звучат идеально, но нелепо трудно понять их в системах сложных типов. Это может фактически сделать проверку типов неразрешимой. Я не знаком со всеми деталями системы типов OCaml, но полностью эквикуркурсные типы в Haskell могут вызвать произвольный цикл проверки типов, пытаясь объединить типы, по умолчанию Haskell гарантирует, что проверка типов прекращается. Более того, в Haskell синонимы типов type T = T -> ()глупы , наиболее полезные рекурсивные типы будут определены следующим образом , однако они встроены в Haskell почти сразу, но вы не можете встроить рекурсивный тип, это бесконечно! Следовательно, рекурсивные типы в Haskell потребуют огромного пересмотра того, как обрабатываются синонимы, вероятно, не стоит того, чтобы даже использовать его как расширение языка.

Изо-рекурсивные типы немного неудобны в использовании, вам более или менее необходимо явно указать средству проверки типов, как складывать и разворачивать ваши типы, делая ваши программы более сложными для чтения и записи.

Тем не менее, это очень похоже на то, что вы делаете с вашим Muтипом. Rollскладывается и unrollразворачивается. На самом деле, у нас есть встроенные изорекурсивные типы. Однако эквикурсивные типы слишком сложны, поэтому такие системы, как OCaml и Haskell, заставляют вас передавать рекурсии через точки фиксации на уровне типов.

Теперь, если вас это заинтересует, я бы порекомендовал типы и языки программирования. Моя копия лежит у меня на коленях, когда я пишу это, чтобы убедиться, что у меня есть правильная терминология :)

Даниэль Гратцер
источник
В частности, в главе 21 дается хорошая интуиция для индукции, коиндукции и рекурсивных типов
Даниэль Гратцер,
Спасибо! Это действительно увлекательно. В настоящее время я читаю TAPL, и я рад слышать, что об этом будет рассказано позже в книге.
бета
@beta Да, TAPL и его старший брат, продвинутые темы по типам и языкам программирования, являются замечательными ресурсами.
Даниэль Гратцер
2

В OCaml вам нужно передать -rectypesв качестве параметра компилятору (или ввести #rectypes;;на верхнем уровне). Грубо говоря, это отключит «происходящую проверку» во время объединения. Ситуация The type variable 'a occurs inside 'a -> 'bбольше не будет проблемой. Система типов по-прежнему будет «правильной» (звуковой и т. Д.), А бесконечные деревья, возникающие как типы, иногда называют «рациональными деревьями». Система типов становится слабее, то есть становится невозможно обнаружить некоторые ошибки программиста.

См. Мою лекцию по лямбда-исчислению (начиная со слайда 27) для получения дополнительной информации об операторах точек фиксации с примерами в OCaml.

lukstafi
источник