Pretty printer for custom data types best practices?

I’m not aware of any particular resource for this. Having tried to write something similar in the past, I’m aware that it’s difficult to give a concise / readable explanation that is appropriate for a broad audience. If someone could do that convincingly, I think it’d make an excellent section of Real World OCaml.

Having said that, I’m happy to regurgitate some of my thoughts on this matter:


Pretty-printers for an abstract type

The “standard” pretty-printer for an abstract type takes a formatter and a value to be formatted, returning unit:

type 'a pretty_printer = Format.formatter -> 'a -> unit

Format comes with stock pretty-printers of this type (with names of the form pp_print_<foo>) but since Format doesn’t define the pretty_printer type explicitly it’s a bit folkloric. (The fmt library defines this type as Fmt.t.) So a given type t will often come with an accompanying value pp of type t pretty_printer:

val pp : t pretty_printer

(* or, for polymorphic containers ['a t]: *)
val pp : 'a pretty_printer -> 'a t pretty_printer

(pp is sometimes called pp_dump in order to emphasise that the format is not stable or that it contains debugging information that would otherwise be hidden.)

Using pretty printers

These pretty printers can be used directly as functions, which is sometimes useful when all you want to do is pretty-print a value, either by itself or part of a sequence of print statements:

let () =
  Format.pp_print_string Format.std_formatter "This is my value: ";
  Library.pp Format.std_formatter Library.value;
  Format.pp_print_flush Format.std_formatter ()

It’s rare to use pretty-printers in their raw form like this, though. The real reason that pretty_printer is the conventional type of pretty-printers is that it works well with the “%a” conversion specification, which expects a pretty_printer of values of some type and a value of that type:

let () =
  Format.printf "This is my value: %a%!" Library.pp Library.value

Defining pretty-printers by hand

The best way to write a pretty-printer is to not bother and let a PPX such as ppx_deriving.show generate one for you. If you have too much time on your hands, there are broadly two ways to define pretty-printers manually:

  • function-level; using a library of combinators such as fmt to do point-free composition of pretty-printers. If you do use fmt, one useful tip is that Fmt.Dump contains useful high-level combinators for containers and records.

  • value-level; writing a function that takes a formatter and a value and makes one-or-more calls to Format.fprintf with that formatter. Here the choice of implememtation is up to you, but here’s a standard approach for records and variants:

type record = { foo : string; bar : int; baz : abstract }
type variant = Foo of string | Bar of int | Baz of abstract

let pp_record ppf r =
  Format.fprintf ppf "{ foo: %s; bar: %d; baz: %a }" r.foo r.bar pp_abstract
    r.baz

let pp_variant ppf = function
  | Foo s -> Format.fprintf ppf "Foo %s" s
  | Bar i -> Format.fprintf ppf "Bar %d" i
  | Baz a -> Format.fprintf ppf "Baz %a" pp_abstract a

or, with breakable spaces and boxes around recursively-invoked pretty-printers:

let pp_record ppf r =
  Format.fprintf ppf "{@ foo:@ %s;@ bar:@ %d;@ baz:@ @[%a@]@ }" r.foo r.bar
    pp_abstract r.baz

let pp_variant ppf = function
  | Foo s -> Format.fprintf ppf "Foo@ %s" s
  | Bar i -> Format.fprintf ppf "Bar@ %d" i
  | Baz a -> Format.fprintf ppf "Baz@ @[%a@]" pp_abstract a

Without going into the details of Format: using breaks and boxes helps your pretty-printers compose more gracefully, but this composability has its limits.

Isn’t this fundamentally backwards?

Given the complexity of Format and the resulting potential for inconsistencies and misuse of boxes and breaks, this convention of defining pretty-printers via Format clearly has issues. There are more fundamental problems though; even if you use a PPX to automatically derive them, there’s no getting around the following issues:

  • it’s impossible to add a pretty-printer for an abstract type that you don’t control;

  • the controller of the abstract type must pick the style of the pretty printer (OCaml-esque, JSON-esque, S-expression etc.), which means there’s no hope of achieving ecosystem-wide consistency.

This is in some sense a fundamental limitation of true abstraction, but to plug my own preferred workaround: there’s a more satisfying story for pretty-printers if you embrace run-time type representations and generic programming. If an abstract type comes along with a type representation – a value representing the internal structure of that type – it becomes possible for the consumer of an abstract type to pick the style of pretty-printer that they want:

(** Generic JSON pretty-printer *)
val json_printer_for : 'a Typerep.t -> 'a pretty_printer

module Library : sig
  type t
  val t : t Typerep.t (* ... likely generated by a PPX *)
  val value : t
end

let () =
  Format.fprintf "My library value: %a%!" 
      (json_printer_for Library.t) Library.value

Providing that such type representations are always provided for the types that you care about, it becomes easy to add any number of pretty-printers for those types after the fact. This is the approach that we use in Irmin and related libraries, and we find it works well there.

18 Likes