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;...]。
但是,当然,它会失败,因为它的运行将无休止并最终导致堆栈溢出。
那么为什么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 an int 2 and a Node of int 3 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 which
将 name1 绑定到循环列表 1::2::1::2::…,并将 name2 绑定到循环
清单 2::1::2::1::…非正式地,接受定义的类别
由仅出现已定义名称的那些定义组成
在函数体内或作为数据构造函数的参数。
如果您使用let rec 定义绑定,请说let rec name。这个name 只能在函数体或数据构造函数中。
在前面的两个示例中,circle1 位于函数体 (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