4

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?

vog
  • 23,517
  • 11
  • 59
  • 75
  • Why do you think that there is one? Most languages (perhaps until recently) only have `try` constructs. – OrangeDog Jun 11 '18 at 09:06
  • @OrangeDog 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) – vog Jun 11 '18 at 09:07
  • 4
    Possible duplicate of [Emulating try-with-finally in OCaml](https://stackoverflow.com/questions/11276985/emulating-try-with-finally-in-ocaml) – Daiwen Jun 11 '18 at 09:21
  • @Daiwen Thanks for pointing out that other question. However, my question asks for an idiom that is one level of abstraction above try-finally. – vog Jun 11 '18 at 10:15
  • @OrangeDog Any language that provides a RAII-like feature (https://stackoverflow.com/questions/2321511/what-is-meant-by-resource-acquisition-is-initialization-raii) can provide with-statement-like syntax. So it's more common than you'd think. – Dunes Jun 11 '18 at 13:03
  • 1
    @OrangeDog 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. – coredump Jun 11 '18 at 13:06
  • @Dunes I was considering RAII, for which only C++ came to mind. – OrangeDog Jun 11 '18 at 13:06
  • @coredump a recent trend to add `with` constructs (e.g. to Python and Java) – OrangeDog Jun 11 '18 at 13:09
  • 2
    @vog 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? – coredump Jun 11 '18 at 13:09
  • 1
    @vog 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. – Mulan Jun 11 '18 at 16:18
  • @coredump 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? – vog Jun 12 '18 at 10:45
  • @vog The idiom seems to be: reimplement it. https://github.com/search?l=OCaml&q=try_finally&type=Code – coredump Jun 12 '18 at 11:56

5 Answers5

9

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).

Yawar
  • 11,272
  • 4
  • 48
  • 80
  • 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. – vog Jun 04 '21 at 12:45
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

loxs
  • 1,476
  • 1
  • 12
  • 27
  • 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.) – vog Jun 12 '18 at 10:51
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")

Chris Vine
  • 677
  • 3
  • 7
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.

Goswin von Brederlow
  • 11,875
  • 2
  • 24
  • 42
  • You probably want `with` not `width` (well, it did confuse me). Also, are you sure about the `raise res` (instead of `raise exn`)? – Dirk Jun 11 '18 at 11:05
  • also `constructor` is a keyword in OCaml – Étienne Millon Jun 11 '18 at 11:10
  • I appended underscores to the identifiers which happened to be reserved keywords. – Martin Jambon Jun 11 '18 at 18:58
  • Since when is constructor a keyword?: OCaml version 4.02.3 # let constructor = 1;; val constructor : int = 1 – Goswin von Brederlow Jun 12 '18 at 07:15
  • 1
    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 (https://www.python.org/dev/peps/pep-0343/#specification-the-with-statement). – coredump Jun 12 '18 at 08:10
  • 1
    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. – Goswin von Brederlow Jun 12 '18 at 08:29
-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
coredump
  • 37,664
  • 5
  • 43
  • 77