OCaml中与Python的 "with"语句(自动释放资源)相对应的是什么?

4

OCaml中与Python的"with"语句对应的是什么?

with open('test.txt', 'r') as f:
    # Do stuff with f
# At this point, f will always be closed, even in case of exceptions

那就是:在OCaml中,如何安全地确保某个资源(打开文件、数据库连接、HTTP连接等)在特定时刻总是被释放?在这里等待垃圾回收器不是一个选项,并且异常也不应该防止资源被释放。当然,在OCaml中,你总是可以使用try-finally并手动关闭文件,就像在Python中一样。然而,这种代码容易出错。这就是为什么Python引入了“with”语句的原因。那么,在OCaml中,如何使这种代码更易于阅读并减少错误呢?请注意,这个问题与在OCaml中模拟try-with-finally的问题非常不同,因为这是进一步的一步:我不仅想在OCaml中模拟try-finally!(顺便说一下,Lwt的[%finally ...]做得很好。)我想进一步,消除在第一次编写这些finally子句的需要 - 就像在Python中一样。请注意,这个问题不涉及实现细节,而是关于惯用法:所有可能的设计和解决方案中,哪些在OCaml社区中得到了一些推广并被普遍接受?

@Daiwen 谢谢你指出另一个问题。但是我的问题要求一个比try-finally更高一级抽象的习惯用语。 - vog
1
@OrangeDog 可能最近流行添加 finally 子句(我不知道),但这个想法并不新鲜:MacLisp(1965)就有 unwind-protect。 - coredump
2
@vog,我真的看不出你的问题与链接中的问题有何不同。你能解释一下另一个问题和答案中没有涉及到的部分吗? - coredump
1
@vog,我认为你没有理解函数式编程的要点。你把Python中的特性当成黑盒魔法一样对待,但是你可以看到,使用某种技术(比如链接的重复示例中的unwind示例),很容易实现这些特性。一旦你在一个函数中编码了所需的行为,你就可以在需要该行为的任何地方重用这个函数,并且该函数的复杂性被隐藏在实现细节中,从调用者的视线中消失了。 - Mulan
显示剩余7条评论
5个回答

9
现在有 Fun.protect,可以被视为(实际上)该习惯用法,因为它在标准库中。例如:
let get_contents file =
  let ch = open_in file in
  Fun.protect ~finally:(fun () -> close_in ch) begin fun () ->
    let len = in_channel_length ch in
    let bytes = Bytes.create len in
    ignore (input ch bytes 0 len);
    bytes
  end

现在,甚至出现了let操作符,它正在逐渐成为更频繁使用的方式,例如:https://github.com/ocaml/ocaml/pull/9887 因此,您可以定义一个let操作符来使用文件,例如:
let ( let& ) ch fn =
  Fun.protect ~finally:(fun () -> close_in ch) begin fun () ->
    fn ch
  end

然后像这样使用:

let get_contents file =
  let& ch = open_in file in
  let len = in_channel_length ch in
  let bytes = Bytes.create len in
  ignore (input ch bytes 0 len);
  bytes
let& 运算符确保在当前作用域 (get_contents) 结束时关闭 in_channel

1
谢谢你指出这个问题!顺便说一下,多年来我最终也将类似的东西放入了我的主要monad(s)中。 - vog

5

谢谢!我想,在广泛使用的库中实现该模式就是我们能够接近的了。我们是否也可以在其他库中观察到相同的模式?(例如电池、数据库库等) - vog

3

这里有一些合理的答案,涉及到it技术和ocaml语言中模拟try with finally的方法(点击此处)。然而,由于缺少宏,使得这种方法相对更加繁琐,与其他情况相比略显麻烦(例如在此处的“无宏”下述投诉)。


0
在Python中,“with”使用特殊对象来管理资源,并在“with”完成时调用清理函数。Ocaml没有这样的功能,但是您可以实现它们。
类似于以下内容:
let with_ (constructor, destructor) fn =
   let obj = constructor () in
   try
       let res = fn obj in
       destructor obj;
       res
   with exn ->
       destructor obj;
       raise exn

或者使用一个具有销毁方法的OCaml对象。

归根结底,您将finally子句隐藏在析构函数或销毁方法中,以便无需手动编写它。


你可能想要使用 with 而不是 width(这个也曾经让我困惑过)。另外,你确定要使用 raise res 而不是 raise exn 吗? - Dirk
同时,“constructor”是OCaml中的一个关键字。 - Étienne Millon
我在标识符末尾添加了下划线,这些标识符恰好是保留关键字。 - Martin Jambon
自从构造函数成为关键字以来是什么时候?:OCaml版本4.02.3

let constructor = 1;;

val constructor : int = 1
- Goswin von Brederlow
1
如果析构函数obj抛出异常,则在处理程序中不应再次调用它。例如,在Python中就不是这样做的(https://www.python.org/dev/peps/pep-0343/#specification-the-with-statement)。 - coredump
1
正确。如果您需要一个覆盖所有情况的通用解决方案,您必须更加仔细地实现它。我想Janestrees库中的In_channel.with_file(请参见其他答案)已经做到了这一点,我建议您去那里查看。 - Goswin von Brederlow

-1

另一个简单的实现。

首先,让我们定义一种类型,它表示结果或异常:

type 'a okko = Ok of 'a | Ko of exn

然后,定义capture_errors

let capture_errors fn = try Ok (fn ()) with e -> (Ko e);;

你可以实现unwind_protect

let unwind_protect wind unwind =
  let result = (capture_errors wind) in
  begin
    unwind ();
    match result with
    | Ok (result) -> result
    | Ko (error) -> raise error
  end;;

以上代码会一直执行 unwind 函数。

你可以定义一个通用的 with_ 函数:

let with_ enter leave body =
    let e = enter() in
    unwind_protect
      (fun () -> (body e))
      (fun () -> (leave e)) ;;

例如,with_open_file 可能被定义为:

let with_open_file opener closer file fn =
  with_
    (fun () -> (opener file))
    (fun (chan) -> (closer chan))
    fn

在常见情况下,您可以使用柯里化(curry)open_inclose_in

let with_input_file = with_open_file open_in close_in;;

例如:

with_input "/etc/passwd" input_line

这个问题不是关于实现细节,而是关于语言习惯用法。 - vog

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接