qexat's blog

Context managers in OCaml

Context managers

In 2005, Python 2.5 introduced the with statement, also known as context managers, following a need for a simple way to achieve a pattern that is very common in code that involves dealing with resources: open a resource, maybe perform some initialization, read or write to it, and then make sure it is closed when things are done even when something has failed somewhere.

with open("good_languages.txt", "r") as file:
    file.write("JavaScript")  # oops, the file is not writable!

Despite the exception being raised, the file will still close cleanly under the hood.

Obviously, Python is far from being the only language with this type of feature. They may have different names, but initializers/destructors are the same idea.

#include <print>

class Resource {
  public:
    Resource(int x);
    ~Resource();
};

Resource::Resource(int x) {
  std::println("Initializing...");
}

Resource::~Resource() {
  std::println("Cleaning up...");
}

int main() {
  Resource res(42);  // Initializing...
  std::println("Doing stuff.");
  return 0;
} // Cleaning up...

Binding operators

6 years ago, the binding operator extension (sometimes referred as let-bindings) was added to OCaml in the version 4.08 of the language.

let ( let+ ) = Option.bind

let parse_rgb (raw_contents : string) : (int * int * int) option =
  (* parse_* functions are defined somewhere else *)
  let+ red = parse_red raw_contents in
  let+ green = parse_green raw_contents in
  let+ blue = parse_blue raw_contents in
  Some (red, green, blue)

While often used similarly to Haskell's do notation — that is, for monadic binding — it is actually more general and powerful.

Let-bindings are special operators of the form let<op> where <op> is some operator like + or $*. You can find the grammar here.

One key thing to remember is their signature. We can look at how they will be used to understand how to define them.

let<op> resource : 'b = (argument : 'a) in
(do_something resource : 'c)  

In our case, 'a will be the argument (for example, the path of the file) to construct the resource object which is 'b, and 'c will be whatever is returned at the end of the operations (for example, a JSON object that was deserialized from the file contents).

Okay, let's get more concrete by putting it into code.

Starting with basics

We set up a module for our type's definition. It is a simple example that doesn't involve actual resources, but I think it's still enough for the demonstration.

module Resource = struct
  type t = { path : string }
end

As it is a resource type, it needs some basic functions like opening, reading and closing.

  (* right below our type definition *)
  let open' (path : string) : t option = Some { path }
  let close (resource : t) : unit = ()
  let read_all (resource : t) : string option = Some "Hello, World!"

Let's try it out:

let maybe_resource = Resource.open' "my_path"
let maybe_contents = Option.bind maybe_resource Resource.read_all

let () =
  match maybe_contents with
  | None -> Printf.eprintf "Something went wrong :(\n"
  | Some contents -> Printf.printf "%s\n" contents
;;

This works fine, we are correctly getting Hello, World! printed on the screen. But we can do better!

Using let-bindings to emulate context managers

First, I will start by making use of a let-binding for Option's monadic binding:

(* above all the code *)
let ( let+ ) = Option.bind

Under the module functions, we will add a Notation submodule where we will define a let-binding that acts like a context manager.

(* in module Resource *)
  module Notation = struct
    let ( let-> ) (path : string) (func : t -> 'a option) : 'a option =
      let+ resource = open' path in
      Printf.printf "Initializing...\n";
      (* We don't use let+ here! We want to close the resource! *)
      let result = func resource in
      Printf.printf "Doing some work!\n";
      close resource;
      Printf.printf "Cleaning up...\n";
      result
    ;;
  end  

All it does is:

This is how the use site looks like now:

open Resource.Notation

let maybe_contents =
  let-> resource = "my_path" in
  Resource.read_all resource
;;

let () =
  match maybe_contents with
  | None -> Printf.eprintf "Something went wrong :(\n"
  | Some contents -> Printf.printf "%s\n" contents
;;

Now, we have something strikingly similar to Python's with statement and it's nice.

Possible enhancements

Admittedly, this is a pretty basic implementation with some room for improvement. For example, it is completely blind to exceptions, so the file doesn't get closed if func raises one. Furthermore, if func accidentally closes the file, it will attempt to do it again without checking, which can be highly problematic.

Full implementation

let ( let+ ) = Option.bind

module Resource = struct
  type t = { path : string }

  let open' (path : string) : t option = Some { path }
  let close (resource : t) : unit = ()
  let read_all (resource : t) : string option = Some "Hello, World!"

  module Notation = struct
    let ( let-> ) (path : string) (func : t -> 'a option) : 'a option =
      let+ resource = open' path in
      Printf.printf "Initializing...\n";
      (* We don't use let+ here! We want to close the resource! *)
      let result = func resource in
      Printf.printf "Doing some work!\n";
      close resource;
      Printf.printf "Cleaning up...\n";
      result
    ;;
  end
end

open Resource.Notation

let maybe_contents =
  let-> resource = "my_path" in
  Resource.read_all resource
;;

let () =
  match maybe_contents with
  | None -> Printf.eprintf "Something went wrong :(\n"
  | Some contents -> Printf.printf "%s\n" contents
;;

Thank you for reading through!

#C++ #binding operators #context managers #destructors #ocaml #python