DEV Community

Yawar Amin
Yawar Amin

Posted on

How to print anything in OCaml

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 >;;
Enter fullscreen mode Exit fullscreen mode

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;
  ];;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 an int. We used it above as part of the definition of the pp_t above, as the printer for the value of the x method
  • val string : string Fmt.t: this one knows how to display a string

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.

Oldest comments (0)