类型检查和递归类型(在Haskell / Ocaml中编写Y组合器)


21

当在Haskell的上下文中解释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?提出该类型后,我想问题是类型(t2)指向自身的右侧?
  • 其次,也许是最有趣的是,Haskell / Ocaml类型的系统不允许这样做的原因是什么?我想这有充分的理由的,因为即使在给定开关的情况下,即使Ocaml 可以处理递归类型,它默认不会允许它-rectypes

如果这些真的是很重要的话题,我将感谢相关文献的指导。

Answers:


16

首先,GHC错误,

GHC试图通过统一一些约束x,首先,我们将其用作函数

x :: a -> b

接下来,我们将其用作该函数的值

x :: a

最后,我们将其与原始参数表达式统一起来,这样

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

现在x x成为统一的尝试t2 -> t1 -> t0,但是,我们无法统一它,因为这需要将t2的第一个参数统一xx。因此,我们的错误信息。

接下来,为什么不使用一般的递归类型。首先要注意的是等值和等价递归类型之间的区别,

  • 您期望mu X . Type的等价递归完全等同于任意扩展或折叠它。
  • 异递归类型提供一对运营商的,fold并且unfold其折叠和展开的类型的递归定义。

现在,等递归类型听起来很理想,但是在复杂类型系统中很难正确地做到这一点。实际上,它会使类型检查变得不确定。我不熟悉OCaml的类型系统的每个细节,但是Haskell中的完全等价类型可能导致类型检查器任意循环以尝试统一类型,默认情况下,Haskell确保类型检查终止。更进一步,在Haskell中,类型同义词是愚蠢的,最有用的递归类型将定义为type T = T -> (),但是在Haskell中几乎立即内联,但是您不能内联递归类型,它是无限的!因此,Haskell中的递归类型将要求对同义词的处理方式进行全面改革,即使作为语言扩展,也可能不值得花很多精力。

等递归类型使用起来有些麻烦,您或多或少不得不显式地告诉类型检查器如何折叠和展开类型,从而使程序的读取和编写更加复杂。

但是,这与您对Mu类型所做的非常相似。Roll折叠,然后unroll展开。因此,实际上,我们确实引入了等递归类型。但是,等递归类型太复杂了,因此OCaml和Haskell之类的系统会迫使您通过类型级别的固定点传递递归。

现在,如果您对此感兴趣,我将推荐类型和编程语言。我在写此书时是为了确保我使用正确的术语,所以我的书在我的膝盖上打开了:)


特别是第21章为归纳,共归和递归类型提供了很好的直觉-Daniel
Gratzer

谢谢!这真是令人着迷。我目前正在阅读TAPL,很高兴听到本书稍后会对此进行介绍。
2013年

@beta Yep,TAPL和他的兄弟,类型和编程语言高级主题,是很棒的资源。
Daniel Gratzer 2013年

2

在OCaml中,您需要将-rectypes参数作为参数传递给编译器(或#rectypes;;在顶层输入)。粗略地说,这将在统一期间关闭“发生检查”。这种情况The type variable 'a occurs inside 'a -> 'b将不再是问题。类型系统仍然是“正确的”(声音等),随类型而出现的无限树有时称为“理性树”。类型系统变得较弱,即无法检测到某些程序员错误。

请参阅我关于lambda演算的讲座(从幻灯片27开始),以获取有关OCaml中的示例的定点运算符的更多信息。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.