有没有办法获得对可变结构体字段的“引用”

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

提问人:Kris 提问时间:4/24/2022 更新时间:5/7/2022 访问量:283

问:

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

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>

有没有办法从可变字段中获取“引用”,以便我可以将其传递给函数。即我想做这样的事情:

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

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

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

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

参考 OCAML公司 记录 可变

评论

0赞 Kris 4/24/2022
作为一种解决方法,我用一个不可变的字段替换了我的字段,其中包含一个 .这或多或少是一回事,但有一个优点,当你这样做时,你得到的默认是一个引用,你必须显式地取消引用它才能获得值。这是“还可以的”,尽管我认为这并不完全是我问题的答案。它更像是一种“解决方法”,完全避免了这个问题,但不使用可变字段。mutablerefapoint.x

答:

3赞 Jeffrey Scofield 4/24/2022 #1

没有办法完全按照你的要求去做。引用的类型非常具体:

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

引用是具有一个名为 的可变字段的记录。你不能从其他记录的任意可变字段中真正捏造出来。即使你愿意对类型系统撒谎,记录的字段也与记录的表示方式完全不同。contents

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

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

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

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

评论

0赞 Kris 4/24/2022
“如果一个API是以命令式风格设计的,那么在OCaml中就很难使用”,我不反对。但是,在 api(真正的 api 而不是简化的稻草人示例)中,突变的作用是将值替换为表示相同语义含义的值的简化版本。从本质上讲,所讨论的数据结构是一个图,我们正在用等效/简化的子图替换子图。因此,从某种意义上说,这不是“真正的”副作用,因为 api 对用户的意义是不变的。只有它的内部表示“秘密”得到了优化。
0赞 Jeffrey Scofield 4/24/2022
我相信你知道你在做什么:-)但是,如果你还没有读过不可变的数据结构,你应该读。令人惊讶的是,在不牺牲太多性能的情况下,可以获得大量的清晰度。真的,我很惊讶。这就是我从 C 切换到 OCaml 的原因。
0赞 Kris 4/24/2022
嘿,我喜欢不可变的数据结构。我喜欢 ocaml 内置且高效的不可变 Map 实现。当我使用 maps 在 Java 中编写代码时,每次输入值后,我都会经常想克隆它们,但这样做的成本有点太高了。所以。。。Ocaml中的不可变地图,我喜欢它:-)
0赞 jthulhu 4/24/2022 #2

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

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

当然没有它所能达到的效率(也不优雅),但它有效。

评论

0赞 Kris 4/24/2022
随着解决方法的进行,这可能是我最不喜欢的一个。但感谢您的建议。我很高兴看到一些替代方案,因为我们似乎缺乏一种方法来完全按照我的意愿去做。
0赞 Kris 4/24/2022 #3

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

这是另一种可能有效的解决方法,如果您可以将函数的实现更改为使用“自制”ref 类型。这与要求非常接近。increment

我们可以定义自己的引用类型,而不是让它采用“内置”引用。“参考”的精神是你可以设置和获得的东西。因此,我们可以将其表征/表示为 a 和 function 的组合。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>

增量函数的代码可以保持不变,即使它的类型“看起来”相同(但它微妙地“不同”,因为它现在使用我们自己的类型而不是内置的 ref)。ref

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 = ()
1赞 Goswin von Brederlow 5/7/2022 #4

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

但你也可以从GC(垃圾回收器)的角度来看它。在 ocaml 内部,所有内容都是存储为 31/63 位值的平凡类型(int、bool、char 等),或者是指向内存块的指针。每个内存块都有一个标头,用于描述 GC 的内容,并具有 GC 使用的一些额外位。

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

但是,让我们假设您可以传递给一个引用的函数。然后在内部,指针将指向 的中间,当 GC 尝试访问该块的标头时,它将失败,因为它不知道标头与指针的偏移量。apoint.yapoint

现在如何解决这个问题?

已经提到的一种方法是使用引用而不是可变。另一种方法是使用吸气器和二传器:

# 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>

如果你只想变量,你可以传递一个增量函数而不是 getter/setter。或任何其他帮助程序函数的集合。getter/setter pait 只是最通用的接口。incr

评论

0赞 Kris 5/7/2022
GC 的观点很有趣。谢谢!至于解决方法,我认为它与我自己的答案中给出的基本上相同(即“模型”通过 getter/setter 对可变事物的引用。在我的回答中,我什至定义了 and 运算符,以使使用它们的代码在语法上与使用 ref 相同。!:=