(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.
let read_lines inc =
let rec loop acc =
try
let l = input_line inc in
loop (l :: acc)
with End_of_file -> List.rev acc
in
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:
let read_lines inc =
let rec loop acc =
match (try Some (input_line inc)
with End_of_file -> None)
with
| Some l -> loop (l :: acc)
| None -> List.rev acc
in
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.
let read_lines inc =
let rec loop acc =
match input_line inc with
| l -> loop (l :: acc)
| exception End_of_file -> List.rev acc
in
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:
let read_lines inc =
In_channel.fold_lines inc ~init:[] ~f:(fun l x -> x :: l)
|> List.rev
Or you could just call In_channel.read_lines
!)