有没有办法获得可变结构字段的 'reference'

Is there a way to obtain a 'reference' to a mutable struct field

所以我有一个带有可变字段的记录类型:

type mpoint = { mutable x:int ; mutable y: int };;
let apoint = { x=3 ; y=4};;

我有一个函数需要 'ref' 并对它的内容做一些事情。 例如:

let increment x = x := !x+1;;
val increment : int ref -> unit = <fun>

有没有办法从可变字段中获取 'reference',以便我可以将其传递给函数。 IE。我想做类似的事情:

increment apoint.x;; (* increment value of the x field 'in place' *)
Error: This expression has type int but an expression was expected of type
         int ref

但是上面的方法不起作用,因为 apoint.x returns 字段的值不是它的 'ref'。如果这是 golang 或 C++,也许我们可以使用 & 运算符来指示我们想要地址而不是字段的值:&apoint.x.

(如何)我们可以在 Ocaml 中做到这一点?

PS:是的,我知道以这种方式避免使用副作用可能更常见。但我保证,我这样做是有充分理由的,因为它比这个 simplified/contrived 示例所暗示的更有意义。

无法完全按照您的要求进行操作。引用的类型非常具体:

# let x = ref 3
val x : int ref = {contents = 3}

引用是一条记录,其中包含一个名为 contents 的可变字段。您不能真正从其他记录的任意可变字段中伪造它。即使你愿意对类型系统撒谎,记录的字段与记录完全不同。

您可以将您的字段声明为实际引用:

type mpoint = { x: int ref; y: int ref; }

那就没问题了,apoint.x确实是个参考。但是这种表示效率不高,即它需要更多的内存并且有更多的解引用来访问值。

如果 API 是以命令式风格设计的,将很难在 OCaml 中使用。反正我就是这么看的。另一种说法是整数很小。该接口或许应该接受一个 int 和 return 一个新的 int,而不是接受对 int 的引用并就地修改它。

您可以随时复制字段的内容,调用函数,然后再返回:

let increment_point_x apoint =
  let x = ref apoint.x in
  increment x;
  apoint.x <- !x

当然没有尽可能高效(也不优雅),但它有效。

不可能完全按照问题的要求去做(@JeffreyScofield 解释了原因,所以我不会重复)。已提出一些解决方法。

如果您可以更改 increment 函数的实现以使用 'home made' ref 类型,则这是另一个可能有效的解决方法。这与要求的非常接近。

我们可以定义自己的引用类型,而不是让它接受 'built-in' 引用。 'reference' 的精神是可以设置和获取的。所以我们可以 characterise/represent 它作为 getset 函数的组合。

type 'a ref = {
  set: 'a -> unit;
  get: unit -> 'a;
};;
type 'a ref = { set : 'a -> unit; get : unit -> 'a; }

我们可以在此类型上定义常用的 !:= 运算符:

let (!) cell = cell.get ();;
val ( ! ) : 'a ref -> 'a = <fun>

let (:=) cell = cell.set;;
val ( := ) : 'a ref -> 'a -> unit = <fun>

增量函数的代码可以保持不变,即使它的类型 'looks' 相同(但它是微妙的 'different' 因为它现在使用我们自己的类型 ref 而不是 built-in 参考).

let increment cell = cell := !cell + 1;;
val increment : int ref -> unit = <fun>

当我们想要引用一个字段时,我们现在可以创建一个。例如一个引用 x:

的函数
let xref pt = {
  set = (fun v -> pt.x <- v);
  get = (fun () -> pt.x); 
};;
val xref : mpoint -> int ref = <fun>

现在我们可以在 x 字段上调用 ​​increment

increment (xref apoint);;
- : unit = ()

Jeffrey Scofield 从类型系统的角度解释了为什么这不能在 ocaml 中完成。

不过你也可以从GC(垃圾收集器)的角度来看。在 ocaml 内部,所有内容要么是存储为 31/63 位值的普通类型(int、bool、char、...),要么是指向内存块的指针。每个内存块都有一个 header 向 GC 描述内容,并有一些额外的位供 GC 使用。

当您在内部查看引用时,它是指向内存块的指针,该内存块包含带有 mutable contents 的记录。通过该指针,GC 可以访问 header 并且知道内存块仍然可以访问。

但是我们假设您可以将 apoint.y 传递给采用引用的函数。然后在内部指针将指向 apoint 的中间,当 GC 尝试访问该块的 header 时,GC 将失败,因为它不知道 header 指针的偏移量是多少]位于。

现在如何解决这个问题?

已经提到的一种方法是使用引用而不是可变的。另一种方法是使用 getter 和 setter:

# type 'a mut = (unit -> 'a) * ('a -> unit);;
type 'a mut = (unit -> 'a) * ('a -> unit)

# type mpoint = { mutable x:int ; mutable y: int };;
type mpoint = { mutable x : int; mutable y : int; }

# let mut_x p = (fun () -> p.x), (fun x -> p.x <- x);;
val mut_x : mpoint -> (unit -> int) * (int -> unit) = <fun>

# let mut_y p = (fun () -> p.y), (fun y -> p.y <- y);;
val mut_y : mpoint -> (unit -> int) * (int -> unit) = <fun>

如果您只想 incr 变量,您可以传递一个增量函数而不是 getter/setter。或任何其他 collection 辅助函数。 getter/setter pait 只是最通用的界面。