What is the OCaml counterpart to Python's "with"-statement (automatic release of resources)
Asked Answered
N

5

5

What is the OCaml counterpart to Python's "with"-statement?

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

That is: What is the preferred way in OCaml to safely ensure that a certain resource (open file, database connection, HTTP connection, etc.) will always be freed at a certain point in time? Waiting for the garbage collector is no option here, and exceptions should never prevent resources from being freed.

Of course, in OCaml you can always use try-finally and close the file "by hand", as you can do in Python. However, that kind of code is prone to errors. This is why Python introduced the "with"-statement. What's the OCaml idiom to make this kind of code easier to read and less prone to errors?

Note that this question is very different from the question Emulating try-with-finally in OCaml, as this is one step further: I don't just want to emulate try-finally in OCaml! (where Lwt's [%finally ...] does a fine job, by the way.) I want to get one step further, eliminating the need to write those finally-clauses in the first place - as one can do in Python.

Also note that this question is not about implementation details, but about idioms: Which of all possible designs and solutions gained some traction in the OCaml community and is generally accepted?

Nostrum answered 11/6, 2018 at 9:3 Comment(12)
Why do you think that there is one? Most languages (perhaps until recently) only have try constructs.Hospers
@Hospers Please note that I didn't ask for a syntactic construct (I'm quite certain that there is none), but for an idiom. If "try-finally" actually is the idiom, that would be an answer, too. (although a very disappointing one)Nostrum
Possible duplicate of Emulating try-with-finally in OCamlProsaism
@Prosaism Thanks for pointing out that other question. However, my question asks for an idiom that is one level of abstraction above try-finally.Nostrum
@Hospers Any language that provides a RAII-like feature (#2322011) can provide with-statement-like syntax. So it's more common than you'd think.Voice
@Hospers There might be a recent trend to add finally clauses (I don't know), but the idea is not new: MacLisp (1965) had unwind-protect.Tameratamerlane
@Voice I was considering RAII, for which only C++ came to mind.Hospers
@Tameratamerlane a recent trend to add with constructs (e.g. to Python and Java)Hospers
@Nostrum I honestly don't see how your question is different from the linked one. Could you clarify what is not covered by the other question and its answers?Tameratamerlane
@Nostrum I believe you miss the point of functional programming. You treat the features in Python like they are black-box magic, but you can see that it's easy to implement them using a technique, eg the unwind example in the linked duplicate. Once you encode the desired behavior in a function, you can reuse the function wherever that behavior is needed, with complexities of that function hidden away in implementation details that are invisible from the caller.Michaella
@Tameratamerlane I'm aware that there are many ways to implement this on my own. That was not my question. My question was about idioms - that is: Which of all possible designs and solutions gained some traction in the OCaml community and is generally accepted?Nostrum
@Nostrum The idiom seems to be: reimplement it. github.com/search?l=OCaml&q=try_finally&type=CodeTameratamerlane
B
10

There is now Fun.protect which may be considered (effectively) the idiom, since it's in the standard library. E.g.,

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

Nowadays, there are even let-operators which are slowly finding their way into more frequent use, e.g. https://github.com/ocaml/ocaml/pull/9887

So you could define a let-op to use a file, like:

let ( let& ) ch fn =
  Fun.protect ~finally:(fun () -> close_in ch) begin fun () ->
    fn ch
  end

And use it like:

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

The let& operator makes sure the in_channel is closed at the end of the current scope (get_contents).

Balliol answered 19/5, 2021 at 17:12 Comment(1)
Thanks for pointing this out! FWIW, over the years I finally ended up putting something like this into my main monad(s) as well.Nostrum
Q
5

JaneStreet's core_kernel standard library replacement provides exactly what you need in the form of In_channel.with_file. So if by any chance you are using core_kernel, see for usage examples here: https://dev.realworldocaml.org/imperative-programming.html#file-io

Quadragesimal answered 12/6, 2018 at 6:27 Comment(1)
Thanks! Having that pattern implemented in a widely-used library is as close as we can get, I guess. Do we observe the same pattern in other libraries, too? (e.g. Batteries, database libraries, etc.)Nostrum
B
3

There are some reasonable answers here (emulating try with finally in ocaml), although the absence of macros makes it somewhat more cumbersome than would otherwise be the case (see eg this complaint under "No macros")

Bestrew answered 11/6, 2018 at 12:52 Comment(0)
P
0

"with" in python uses special objects that manage resources and have a cleanup function that gets called when the "with" is done. Ocaml has no such thing. But you could implement them.

Something like:

let with_ (constructor, destructor) fn =
   let obj = constructor () in
   try
       let res = fn obj in
       destructor obj;
       res
   with exn ->
       destructor obj;
       raise exn

Or use an ocaml object that has a destroy method.

What it comes down to is that you hide your finally-clauses in the destructor or destroy method so you doin't have to write it manually.

Peccant answered 11/6, 2018 at 10:45 Comment(6)
You probably want with not width (well, it did confuse me). Also, are you sure about the raise res (instead of raise exn)?Futile
also constructor is a keyword in OCamlDuce
I appended underscores to the identifiers which happened to be reserved keywords.Grandeur
Since when is constructor a keyword?: OCaml version 4.02.3 # let constructor = 1;; val constructor : int = 1Peccant
If destructor obj throws an exception, you call it a second time in the handler. That is not what is done in Python, for example (python.org/dev/peps/pep-0343/#specification-the-with-statement).Tameratamerlane
Correct. If you need an universal solution that covers all cases you have to implement this much more carefully. I imagine In_channel.with_file from Janestrees library (see other answer) has done that and I refer you there.Peccant
T
-1

Another simple implementation.

Let's first define a type which represents either a result, or an exception:

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

Then, define capture_errors:

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

You can them implement 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;;

The above consistently execute unwind once.

You could then define a generic with_ function:

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

For example, with_open_file might be defined as:

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

You can curry open_in and close_in in the common case:

let with_input_file = with_open_file open_in close_in;;

For example:

with_input "/etc/passwd" input_line
Tameratamerlane answered 12/6, 2018 at 9:25 Comment(1)
The question was not about implementation details, but about language idioms.Nostrum

© 2022 - 2024 — McMap. All rights reserved.