camlspotter 的回答已经足够好了。我只想在这里再补充几点。
首先,对于 的问题write a function that receives a finite list and returns an infinite, circular version of it
,可以在代码/实现层面做,只要你真的使用函数,就会有stackoverflow问题,永远不会返回。
您尝试做的一个简单版本是这样的:
let rec circle1 xs = List.rev_append (List.rev xs) (circle1 xs)
val circle: 'a list -> 'a list = <fun>
可以编译,理论上是正确的。上[1;2;3]
,它应该生成[1;2;3;1;2;3;1;2;3;1;2;3;...]
.
但是,当然,它会失败,因为它的运行将是无止境的,最终会出现 stackoverflow。
那么为什么let rec circle2 = 1::2::3::circle2
会起作用呢?
让我们看看如果你这样做会发生什么。
首先,circle2
是一个值,它是一个列表。在 OCaml 获得此信息后,它可以为 circle2 创建一个静态地址,并以列表的内存表示。
内存的真实值是1::2::3::circle2
,实际上是Node (1, Node (2, Node (3, circle2)))
,即A Node with int 1 and a Node of int 2 address and a Node of int 3 address and address of circle2。但是我们已经知道 circle2 的地址,对吧?所以 OCaml 只是把 circle2 的地址放在那里。
一切都会奏效。
另外,通过这个例子,我们还可以知道这样一个事实,对于这样定义的无限循环列表,实际上并不消耗有限的内存。它不会生成一个真正的无限列表来消耗所有内存,而是当一个循环结束时,它只是“跳回”到列表的头部。
然后让我们回到 的示例circle1
。Circle1 是一个函数,是的,它有一个地址,但我们不需要也不想要它。我们要的是函数应用的地址circle1 xs
。它不像 circle2,它是一个函数应用程序,这意味着我们需要计算一些东西来获取地址。所以,
OCaml 会做List.rev xs
,然后尝试获取地址circle1 xs
,然后重复,重复。
好的,那为什么我们有时会得到Error: This kind of expression is not allowed as right-hand side of 'let rec'
?
来自http://caml.inria.fr/pub/docs/manual-ocaml/extn.html#s%3aletrecvalues
let rec 绑定构造,除了定义递归函数外,还支持某类非函数值的递归定义,比如
let rec name1 = 1 :: name2 and name2 = 2 :: name1 in expr 将 name1 绑定到循环列表 1::2::1::2::…,并将 name2 绑定到循环列表 2::1:: 2::1::…非正式地,接受定义的类由那些定义的名称仅出现在函数体内或作为数据构造函数的参数的定义组成。
如果你let rec
用来定义一个绑定,比如说let rec name
. 这name
只能在函数体或数据构造函数中。
在前面的两个示例中,circle1
is 在函数体 ( let rec circle1 = fun xs -> ...
) 中并且circle2
在数据构造函数中。
如果你这样做let rec circle = circle
,它会给出错误,因为圆不在两种允许的情况下。let rec x = let y = x in y
也不会这样做,因为 x 不在构造函数或函数中。
这里也有明确的解释:
https://realworldocaml.org/v1/en/html/imperative-programming-1.html
部分Limitations of let rec