OCaml 的类型系统会阻止它对教会数字建模吗?

Does OCaml's type system prevent it from modeling Church numerals?

作为消遣,我正在尝试解决我在大学学习的课程(与 Lambda 微积分和各种编程概念有关)中出现的各种问题。因此,我正在尝试在 OCaml 中实现教会数字和相关运算符(也作为 OCaml 中的练习)。

这是目前的代码:

let church_to_int n =
    n (fun x -> x + 1) 0;;

let succ n s z =
    s (n s z)

let zero = fun s z -> z

let int_to_church i =
    let rec compounder i cont =
        match i with
        | 0 -> cont zero
        | _ -> compounder (i - 1) (fun res -> cont (succ res))
    in
    compounder i (fun x -> x)

let add a b = (b succ) a

let mul a b = (b (add a)) zero

所以,它似乎可以工作,但后来就崩溃了。让我们考虑这些定义:

let three = int_to_church 3
let four = int_to_church 4
church_to_int (add three four) // evaluates to 7
church_to_int (add four three) // throws type error - see later

我知道抛出的错误与定义 Church 数字时的类型多态性有关(请参阅 SO question),然后在调用一次闭包后解决。但是,我似乎不明白为什么在这种情况下会抛出类型不一致错误:

let three = int_to_church 3
let four = int_to_church 4
church_to_int (mul three four) // throws type error - see later

有什么想法吗?

具体错误:

1.

Error: This expression has type (int -> int) -> int -> int                                                 but an expression was expected of type                                                                ((('a -> 'b) -> 'c -> 'a) -> ('a -> 'b) -> 'c -> 'b) ->                                    
         ((((int -> int) -> int -> int) -> (int -> int) -> int -> int) ->
          ((int -> int) -> int -> int) -> (int -> int) -> int -> int) ->
         'd
       Type int is not compatible with type ('a -> 'b) -> 'c -> 'a 

2.

Error: This expression has type                                                                              ((((('a -> 'b) -> 'c -> 'a) -> ('a -> 'b) -> 'c -> 'b) ->                                             (('d -> 'd) -> 'd -> 'd) -> 'e) ->                                                       
          ((('a -> 'b) -> 'c -> 'a) -> ('a -> 'b) -> 'c -> 'b) ->
          (('d -> 'd) -> 'd -> 'd) -> 'e) ->
         (((('a -> 'b) -> 'c -> 'a) -> ('a -> 'b) -> 'c -> 'b) ->
          (('d -> 'd) -> 'd -> 'd) -> 'e) ->
         ((('a -> 'b) -> 'c -> 'a) -> ('a -> 'b) -> 'c -> 'b) ->
         (('d -> 'd) -> 'd -> 'd) -> 'e
       but an expression was expected of type
         ((((('a -> 'b) -> 'c -> 'a) -> ('a -> 'b) -> 'c -> 'b) ->
           (('d -> 'd) -> 'd -> 'd) -> 'e) ->
          'e) ->
         ('f -> 'g -> 'g) -> 'h
       The type variable 'e occurs inside
       ((('a -> 'b) -> 'c -> 'a) -> ('a -> 'b) -> 'c -> 'b) ->
       (('d -> 'd) -> 'd -> 'd) -> 'e

如果你看一下三的类型,你会得到:

val three : ('_a -> '_a) -> '_a -> '_a = <fun> 

这简直就是价值限制在起作用。这里很好地解释了值限制:https://realworldocaml.org/v1/en/html/imperative-programming-1.html#side-effects-and-weak-polymorphism

为了解决这个问题,在这种情况下你可以简单地eta-expand函数:

let three x = int_to_church 3 x ;;
val three : ('a -> 'a) -> 'a -> 'a = <fun> 

好吧,我对 lambda 演算有点生疏,但在与一些聪明的老人讨论了几次之后,我得出了这个答案:是的,那样写,OCaml 的类型系统不允许写教会数字。

真正的问题在于你的加法项:

let add a b = b succ a

具有以下类型

(((('a -> 'b) -> 'c -> 'a) -> ('a -> 'b) -> 'c -> 'b) -> 'd -> 'e) -> 'd -> 'e

add 的参数类型不同。这有点可悲,因为我们天真地期望加法是可交换的。 您可以在编写时轻松验证这一点:

let double a = add a a //produces type error - the type variable occurs ...

此错误意味着您正在尝试将一种类型与“更大”的类型统一,即包含它(例如:将 'a 与 'a -> 'a 统一). OCaml 不允许这样做(除非您设置了允许循环类型的 -rectypes 选项)。 为了更好地理解发生了什么,让我们添加类型注释来帮助打字者(为了清楚起见,我将稍微更改一下您的符号):

type 'a church = ('a -> 'a) -> 'a -> 'a
let zero : 'a church = fun f x -> x
let succ n : 'a church = fun f x -> f (n f x)

现在让我们回到 add 术语并稍微注释一下,看看打字员要说什么:

let add (a:'a church) b = a succ b // we only annotate "a" to let the typer infer the rest

这会产生一个非常奇怪的类型:

'a church church -> 'a church -> 'a church

这变得有趣了:为什么第一个参数输入为 'a church church

答案如下:在这里,教堂整数是一个采用 moving 函数的值('a -> 'a)可以浏览一个space,以及一个属于那个space的起点('a)。

这里,如果我们指定参数a的类型是'a church,那么'a 代表我们可以移动的 space。 由于 succamoving 功能在教堂上运行,比 'a 这里本身就是一个'a church,因此参数a an 'a church教堂.

这根本不是我们一开始想要的类型...但这证明了为什么类型系统不允许您的值 threefour 作为 add.

的第一个和第二个参数

一个解决方案是用不同的方式编写 add :

let add a b f x = a f (b f x)

这里,a和b都具有相同的移动功能,因此是同一个类型,但是你不再欣赏偏应用的漂亮文字了...

另一种让您保持这种优美写作的解决方案是使用通用类型,它允许更大种类的多态性:

type nat = {f:'a.('a -> 'a) -> 'a -> 'a}
// this means “for all types ‘a”

let zero : nat = {
  f=fun f x -> x
}

let succ n : nat = {
  f= fun f x -> f (n.f f x)
}

let add (a:nat) (b:nat) = a.f succ b

let double a = add a a //types well

let nat_to_int n =
  n.f (fun x -> x + 1) 0;;

let nat_four = succ (succ (succ (succ zero)))

let eight_i = double nat_four |> nat_to_int //returns 8

但是这个解决方案比你最初的解决方案有点冗长。

希望清楚。