(OCaml 4.02 has branched, which makes it a good time to stop and take a look at what to expect for this release. This is part of a series of posts where I'll describe the features that strike me as notable. This is part 3. You can also check out parts 1 and 2.)

This one is a modest improvement, but a nice one.

Here's a simple bit of code for reading a file line-by-line in OCaml.

  1. let read_lines inc =
  2. let rec loop acc =
  3. try
  4. let l = input_line inc in
  5. loop (l :: acc)
  6. with End_of_file -> List.rev acc
  7. in
  8. loop []

But the above code has a problem: it's not tail recursive, because the recursive call to [loop] is within the exception handler, and therefore not a tail call. Which means, if you run this on a sufficiently large file, it will run out of stack space and crash.

But there's a standard way around this problem, which is to wrap just the input_line call with try_with, and then pattern match on the result. That would normally be done like this:

  1. let read_lines inc =
  2. let rec loop acc =
  3. match (try Some (input_line inc)
  4. with End_of_file -> None)
  5. with
  6. | Some l -> loop (l :: acc)
  7. | None -> List.rev acc
  8. in
  9. loop []

This is an OK solution, but it has some warts. In particular, there's the extra option that gets allocated and immediately forgotten, which can be problematic from a performance perspective. Also, the nesting of the try/with within the match is a bit on the ugly side.

That's where handler-case comes in. Essentially, in 4.02 the match statement and the try-with statement have been combined together into one. Or, more precisely, the match syntax has been extended to allow you to catch exceptions too. That means you can rewrite the above as follows.

  1. let read_lines inc =
  2. let rec loop acc =
  3. match input_line inc with
  4. | l -> loop (l :: acc)
  5. | exception End_of_file -> List.rev acc
  6. in
  7. loop []

This is both more concise and more readable than the previous syntax. And the call to loop is tail-recursive, as one would hope.

(If this isn't obvious, while the above is a good example, it's not what you'd write to solve this problem in practice. Instead, you might use Core's In_channel.fold_lines, as follows:

  1. let read_lines inc =
  2. In_channel.fold_lines inc ~init:[] ~f:(fun l x -> x :: l)
  3. |> List.rev

Or you could just call In_channel.read_lines!)