【问题标题】:Creating a doubly linked list from a list in OCaml从 OCaml 中的列表创建双向链表
【发布时间】:2011-04-27 20:29:21
【问题描述】:

经常有人告诉我,使用 OCaml 中的 Lazy 模块,可以使用诸如 Haskell 之类的惰性语言完成所有您可以做的事情。为了测试这种说法,我正在尝试编写一个函数,将常规列表转换为 ocaml 中的静态双向链表。

type 'a dlist = Dnil | Dnode of 'a dlist * 'a * 'a dlist

鉴于这种类型,我可以手动创建几个静态双向链表:

let rec l1 = Dnode (Dnil,1,l2)
    and l2 = Dnode   (l1,2,l3)
    and l3 = Dnode   (l2,3,Dnil)

但我想编写一个'a list -> 'a dlist 类型的函数,给定任何列表都会在 OCaml 中构建一个静态双向链表。例如,给定[1;2;3],它应该输出与上面的l1 等效的内容。

用 Haskell 编写算法非常简单:

data DList a = Dnil | Dnode (DList a) a (DList a)

toDList :: [a] -> DList a
toDList l = go Dnil l
 where
  go _ [] = Dnil
  go h (x:xs) = let r = Dnode h x (go r xs) in r

但我无法弄清楚在哪里调用 lazy 以使其在 OCaml 中编译。

【问题讨论】:

  • 首先,类型是'a dlist,而不是'a DList。 ;)

标签: ocaml lazy-evaluation


【解决方案1】:

如果您以从右到左的顺序构建链表(与普通列表一样),则每个节点的左侧元素将仅在该节点本身构建后构建。您需要通过使左元素惰性来表示这一点,这意味着“此值将稍后构造”:

type 'a dlist = 
  | Dnil
  | Dnode of 'a dlist Lazy.t * 'a * 'a dlist

一旦你有了这个,使用递归定义将每个节点构造为一个惰性值,该定义将惰性(仍然未构造的)节点传递给构建下一个节点的函数调用(以便它可以访问前一个节点)。它实际上比看起来更简单:

let dlist_of_list list = 
  let rec aux prev = function
    | [] -> Dnil
    | h :: t -> let rec node = lazy (Dnode (prev, h, aux node t)) in 
                Lazy.force node
  in
  aux (Lazy.lazy_from_val Dnil) list

【讨论】:

    【解决方案2】:

    您只能构建在编译时确定的形状的循环不可变严格数据结构。我不打算正式定义或证明这一点,但直观地说,一旦创建了数据结构,它的形状就不会改变(因为它是不可变的)。所以你不能添加到一个循环中。如果创建循环的任何元素,则需要同时创建循环的所有其他元素,因为您不能有任何悬空指针。

    Ocaml 可以做 Haskell 可以做的事情,但你必须让 Lazy 模块参与进来!与 Haskell 不同,ML 的数据结构是严格的,除非另有说明。惰性数据结构具有 'a Lazy.t 类型的片段。 (在特定问题上,ML 的输入比 Haskell 更精确。)惰性数据结构允许通过临时悬空指针(其链接值在指针首次取消引用时自动创建)来构建循环。

    type 'a lazy_dlist_value =
      | Dnil
      | Dnode of 'a lazy_dlist_value * 'a * 'a lazy_dlist_value
     and 'a lazy_dlist = 'a lazy_dlist_value Lazy.t
    

    另一种拥有循环数据结构的常用方法是使用可变节点。 (事实上​​,严格编程的顽固支持者可能会将惰性数据结构视为不会过多破坏引用透明度的可变数据结构的特例。)

    type 'a mutable_dlist_value =
      | Dnil
      | Dnode of 'a mutable_dlist_value * 'a * 'a mutable_dlist_value
     and 'a mutable_dlist = 'a mutable_dlist_value ref
    

    当涉及至少一个可变组件、一个函数(闭包)或有时是模块时,循环数据结构最有用。但是编译器没有理由强制执行这一点——循环严格不可变的一阶数据结构只是一种偶尔有用的特殊情况。

    【讨论】:

      【解决方案3】:
      type 'a dlist = Dnil | Dnode of 'a dlist Lazy.t * 'a * 'a dlist Lazy.t
      let rec of_list list = match list with
          [] -> Dnil
          | x :: [] ->
              let rec single () = Dnode (lazy (single ()), x, lazy (single ()))
              in single ()
          | x :: y -> Dnode (
              lazy (
                  of_list (match List.rev list with
                      [] | _ :: [] -> assert false
                      | x :: y -> x :: List.rev y
                  )
              ),
              x,
              lazy (
                  of_list (match list with
                      [] | _ :: [] -> assert false
                      | x :: y -> y @ x :: []
                  )
              )
          )
      let middle dlist = match dlist with
          Dnil -> raise (Failure "middle")
          | Dnode (_, x, _) -> x
      let left dlist = match dlist with
          Dnil -> raise (Failure "left")
          | Dnode (x, _, _) -> Lazy.force x
      let right dlist = match dlist with
          Dnil -> raise (Failure "right")
          | Dnode (_, _, x) -> Lazy.force x
      

      【讨论】:

      • 这是不正确的。它构建了一个循环双向链表,而不是一个空终止的链表。这很好,但不完全符合我的要求。更重要的是,它无法打结,而是构建了一个惰性无限树,在观察上与我想要的相同,但它不会使用有界内存。特别是 left (right dlist) 不会与 dlist 是同一个对象。
      猜你喜欢
      • 1970-01-01
      • 2020-02-14
      • 1970-01-01
      • 2020-02-14
      • 1970-01-01
      • 2014-03-11
      • 1970-01-01
      • 2016-07-18
      • 2022-01-12
      相关资源
      最近更新 更多