【问题标题】:How do I write a function to create a circular version of a list in OCaml?如何编写一个函数来在 OCaml 中创建一个循环版本的列表?
【发布时间】:2014-10-20 21:50:09
【问题描述】:

可以使用 let rec 创建无限的循环列表,而无需求助于可变引用:

let rec xs = 1 :: 0 :: xs ;;

但是我可以使用同样的技术来编写一个函数来接收一个有限列表并返回它的无限循环版本吗?我试着写了

let rec cycle xs = 
    let rec result = go xs and
            go = function
              | [] -> result
              | (y::ys) -> y :: go ys in
    result
;;

但出现以下错误

错误:这种表达式不允许作为 `let rec' 的右侧

【问题讨论】:

标签: ocaml letrec


【解决方案1】:

你的代码有两个问题:

  • result = go xslet rec 的非法形式
  • 该函数试图通过一些计算创建一个循环,该计算陷入无限循环导致堆栈溢出。

上面的代码被编译器拒绝,因为您不能在let rec 的右侧编写可能导致递归计算的表达式(请参阅Limitations of let rec in OCaml)。

即使你解决了问题,你仍然有问题:cycle 没有完成工作:

let rec cycle xs =
  let rec go = function
    | [] -> go xs
    | y::ys -> y :: g ys
  in
  go xs;;

cycle [1;2];;

cycle [1;2] 因堆栈溢出而失败。

在 OCaml 中,let rec 只有在其定义为“静态”且不执行任何计算时才能定义循环结构。 let rec xs = 1 :: 0 :: xs 就是这样一个例子:(::) 不是函数而是构造函数,它纯粹是构造数据结构。另一方面,cycle 执行一些代码执行来动态创建一个结构,它是无限的。恐怕你不能在OCaml中写cycle这样的函数。

如果你想在 OCaml 中像 cycle 这样的数据中引入一些循环,你可以做的是使用惰性结构来防止像 Haskell 的惰性列表这样的立即无限循环,或者使用突变通过替换来进行循环。 OCaml 的列表不是惰性的也不是可变的,因此你不能写一个函数来动态构造循环列表。

【讨论】:

    【解决方案2】:

    如果你不介意使用黑魔法,你可以试试这个代码:

    let cycle l =
      if l = [] then invalid_arg "cycle" else
      let l' = List.map (fun x -> x) l in   (* copy the list *)
      let rec aux = function
        | [] -> assert false
        | [_] as lst ->   (* find the last cons cell *)
            (* and set the last pointer to the beginning of the list *)
            Obj.set_field (Obj.repr lst) 1 (Obj.repr l')
        | _::t -> aux t
      in aux l'; l'
    

    请注意,强烈建议不要使用 Obj 模块。另一方面,已知有工业级程序和库(Coq、Jane Street's Core、包括电池)使用这种被禁止的艺术。

    【讨论】:

      【解决方案3】:

      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 -&gt; ...) 中,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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-11-16
        • 1970-01-01
        • 1970-01-01
        • 2022-07-12
        • 1970-01-01
        • 2011-02-07
        相关资源
        最近更新 更多