1

我是一个 ocaml 菜鸟。到目前为止,使用普通的旧 ref 到 int 或其他简单的内置类型对我来说在所有方面都符合预期。我在元组的上下文中使用了它们,其中引用是元组的成员。我可以更新 refs,取消引用它们等。

 # let e = 1, ref 1;;
 val e : int * int ref = (1, {contents = 1})
 # snd e := 2;;
 - : unit = ()
 # e;;
 - : int * int ref = (1, {contents = 2})
 # !(snd e);;
 - : int = 2

但是,一旦我向其他聚合(甚至是内置的简单)类型声明了一个命名类型“ref”,事情就会变得很糟糕。我发现我不能像以前那样更改引用,因为它们没有被声明为“引用”类型。这 !和 := 运算符失败。

并且语义似乎以奇怪的明显不一致的方式发生变化。下面只是一个例子。为什么在下面编写第一个代码块是合法的,但在顶部循环(下面进一步)做类似的事情似乎是非法的?第一个块被编译器接受,我们可以在其中匹配一个构造类型的 ref 并使用 ! 第 13 行和第 14 行的运算符。这都在循环队列的上下文中,并使用 #use 从顶部循环中的文件加载:

type 'a element = 'a * 'a pointer 
and 'a pointer = Pointer of 'a element ref;;
let next (_,n) = n;;
type 'a queue = 'a element option ref;;

let create () = None;;
(*passes compiler and behaves well*)
let enqueue queue x = 
  match !queue with
      None ->
    let rec elem = (x, Pointer (ref elem)) in 
    queue := Some elem;
    | Some (_, Pointer last_newest_next) -> (*Insert between newest and oldest*)
      let oldest = !last_newest_next in
      let elem = (x, Pointer (ref oldest)) in
      last_newest_next := elem;
      queue := Some elem;;

在顶部循环中,类似的努力(以及对此的变体)失败了,如下所示,我还使用函数分解了一个元组,然后尝试调用相同的运算符:

let rec elem = (1, Pointer (ref elem));;
let last = !(next elem);;
Characters 12-22:
let last = !(next elem);;
          ^^^^^^^^^^
Error: This expression has type int pointer
   but an expression was expected of type 'a ref

是的,我正在使用 -rectypes 但我想在不使用递归缩写类型的情况下尝试一次,从那以后我就一直坚持下去。请注意以下在顶部循环中的工作,但我不确定它是否等效以及我真正想要的:

let last = next elem;;
val last : int pointer = (* ...  *)

如果第 14 行的第一个代码块被更改为不使用 ! 运营商,它打破了。重写(如下)导致入队函数通过编译器但行为不端:

 (*compiles but fails - que only ever holds one item*)
let enqueue queue x = 
  match !queue with
      None ->
    let rec elem = (x, Pointer (ref elem)) in 
    queue := Some elem;
    | Some (_, Pointer last_newest_next) ->
      let oldest = last_newest_next in
      let elem = (x, Pointer oldest) in
      last_newest_next := elem;
      queue := Some elem;;

一定是没有!运算符(以及一些其他更改),倒数第二行实际上是使 elem 中的指针指向自身,而不是更新不同的指针(从匹配的分解元素内)以指向 elem 最初的预期。无论如何,我仍然不明白为什么在类型化 ref 的顶部循环元组分解和从 ml 文件中执行相同操作之间的语义似乎不一致……如果这甚至是所有这一切的原因。还是从模式匹配中分解与通过函数分解元组不同?!?

我使用了一个出列函数来测试上述函数的行为:

let dequeue queue = 
  match !queue with
      None -> raise Not_found
    | Some (_, Pointer oldest_ref) ->
      let oldest = !oldest_ref in
      let (x, Pointer next_ref) = oldest in
      let next = !next_ref in
      if next == oldest then
    queue := None
      else 
    oldest_ref := next;
      x;;

我可以理解为什么我可能想避开函数式语言中的 ref 单元格,但我需要知道在必要时如何使用它们(不是双关语)。

4

2 回答 2

5

我很难在你写的东西中找到一个特定的问题。但是,OCaml 并非不一致或不合逻辑。这是“现代” FP 语言的优点之一——它们的类型系统基于可靠的数学。现在,我将专注于您展示的第一件不起作用的事情:

# let rec elem = (1, Pointer (ref elem));;
# let last = !(next elem);; ## TYPE ERROR HERE

如果你只看是什么,似乎问题就很清楚了next elem。从 , 的定义elem可以next elem看出Pointer (ref elem)。这不是参考。它的构造函数是Pointer. 因此,将运算符应用于它是没有意义的!,这就是类型错误告诉你的。如果要elem退出next elem,则需要解构Pointer构造函数。

# let unpointer (Pointer x) = x;;
# let last = !(unpointer (next elem));;
# last == elem;;
- : bool = true
# 

编辑:对于它的价值,你的循环列表类型对我来说有点令人费解。如果您真的需要循环列表,您可以查看 OCaml Batteries Included: BatDllist中出现的双向链表实现。这是一个简单的低级实现,看起来很像你用 C 编写的。更好的是使用内置的 OCaml 列表!在多年的 OCaml 编码中,我从来没有觉得需要使用循环列表(只有一个数据点)。

于 2012-12-27T06:53:36.603 回答
4

您似乎对编程语言的语义有极低层次的理解。在这种情况下,坚持低位思考似乎会让你误入歧途。OCaml 中引用的语义是一致的,您在这里观察到的所有奇怪现象都是由于您的错误,而不是语言语义。

如果这可以帮助您,这里有一种向低实现级别思考的人描述 OCaml 语义的方法。这不是我通常向初学者描述的方式,但如果你坚持从指针、聚合和指针的角度思考:

  • OCaml 中的值可以用整数表示且不可变,并按原样传递,或者是指向某个堆块的指针;可变性在这种情况下更容易推理,在一种按值/按引用区分的语言中,除非明确解构或重建,否则一切都是共享的

  • 引用是定义为具有可变字段的记录的派生概念type 'a ref = {mutable contents: 'a};,这对应于一个简单的框,该框指向一个值并且可以更改它指向的值;它是多态的并且行为一致。特别是,您观察到的任何打字错误都是由于代码中的错误造成的,请仔细查看!

  • 类型type foo = Foo of t不同于t; 您可以将 av : t转换为foowith Foo v,并将 av : foo转换为 a twith (match v with (Foo x) -> x)

我认为你同时混合了太多的困难。放下-rectypes并让您的代码正常工作,然后您可以考虑将其添加回来,看看它是否可以减轻代码的负担。默认情况下不启用的原因是接受-rectypes了一些肯定是错误的代码(let sum x = x (* + *) x),只是在以后使用它时会导致一个神秘的错误消息。如果您对语言的其他方面感到不舒服,您不希望这种情况发生。

于 2012-12-27T08:35:37.780 回答