ONE of the big benefits of OCaml is its powerful REPL (also called the toplevel), the interactive command-line utility where you can load modules, type in and execute code, and see its results. The modern REPL, utop, has powerful auto-completion and integration with the build system dune, which enables productive workflows like loading an entire project's libraries in the REPL and interactively exploring them.
However, OCaml's strict typing system has a rather large drawback when using it in this interactive way–for many types, their values cannot be displayed in the REPL. This is because the types don't exist at runtime (when the values are calculated). The REPL only knows how to display a limited set of values. When trying to use and explore libraries interactively, this is quite a barrier.
Pretty-printing from the REPL
Fortunately, the REPL can be taught how to pretty-print values of any type, using its built-in #install_printer
command. In the next section we will see how to use that, but first, let's understand what the REPL can display by itself. In the following session, the #
character is the REPL prompt, and lines starting with -
are output:
# false;;
- : bool = false
# 1;;
- : int = 1
# 'a';;
- : char = 'a'
# "hello";;
- : string = "hello"
# Unix.gmtime (Unix.time ());;
- : Unix.tm =
{Unix.tm_sec = 47; tm_min = 20; tm_hour = 18; tm_mday = 4; tm_mon = 5;
tm_year = 122; tm_wday = 6; tm_yday = 154; tm_isdst = false}
# Unix.O_WRONLY;;
- : Unix.open_flag = Unix.O_WRONLY
Above we see that the REPL can display core built-in types, record types, and variant types. It has knowledge of how these types are defined and how to traverse their (sometimes complex) structures.
However, let's try displaying some custom types:
# let inc = open_in "README.md";;
val inc : in_channel = <abstr>
# close_in inc;;
- : unit = ()
# object
method x = 1
end;;
- : < x : int > = <obj>
The REPL doesn't know how to display the value of the abstract type in_channel
, so it shows simply <abstr>
. And it doesn't know how to display the value of an object, so it shows <obj>
. In practice, these are the types we will need to teach it about.
Installing a printer
Earlier we mentioned the #install_printer
command, it works like this:
# #install_printer pp;;
Where pp
is a pretty-printer function of a specific type, pp : Format.formatter -> 'a -> unit
. Where do we get pp
from? Usually, we define it by hand for custom types. Defining these functions for custom types can be a bit tedious; fortunately, the fmt library helps us by abstracting away much of the tedium.
The fmt
library is popular in the ecosystem and chances are you already have it installed in your current opam switch. Let's check:
$ opam list | grep fmt
fmt 0.9.0 OCaml Format pretty-printer combinators
Using fmt
Now, let's try using fmt
to create a pretty-printer for a custom type. For the sake of convenience, we will try it for a simple object type:
# type t = < x : int >;;
First, we define the type we will test with. The specific type is not important; any type would do. The point is to show that we are creating a pretty-printer for a type that the REPL normally cannot display.
# #require "fmt";;
# let pp_t : t Fmt.t =
let open Fmt in
record [
field "x" (fun t -> t#x) int;
];;
Then, we load the fmt
library and define the pretty-printer. The naming convention for pretty-printers is to call them pp_TY
, where TY
is the name of the type that this printer is for. In our case, this is t
.
We define the printer by calling the record
function and passing it a list of 'fields'. Each 'field' is a call to the field
function that provides:
- The name of the field
- How to extract the field from the object
- The type of the field value
You may be wondering, how did we know that we should use the record
function? Well, strictly speaking, record
is meant to be used to define pretty-printers for OCaml record types; however, object types are close enough in nature to record types that I feel it's reasonable to use the same function for them.
# #install_printer pp_t;;
# object
method x = 1
end;;
- : < x : int > = x: 1
Finally, we install the pretty-printer and enter an object of the required type to evaluate and display. The pretty-printer now displays the value of the object, as a key-value pair.
How fmt
works
The above should give you a taste for fmt
, but to get more comfortable with it, we need to understand its design. fmt
provides a set of combinators which can be pieced together like Lego blocks, to build pretty-printer functions of any type t Fmt.t
. It provides building blocks from the simplest ones, e.g.
-
val int : int Fmt.t
: this pretty-printer knows how to display anint
. We used it above as part of the definition of thepp_t
above, as the printer for the value of thex
method -
val string : string Fmt.t
: this one knows how to display astring
To complex combinators which compose together simpler ones, like record
which we saw above.
When to use fmt
In my opinion the most compelling use case for fmt
is creating pretty-printers for the REPL, like above. By default, the actual formatting of the values is very simple, suitable for exploratory work. fmt
provides options to customize the output to a certain extent. However for more complex control and serialization to various formats, in practice you will probably want to use more targeted libraries like a JSON encoder.
Top comments (0)