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 usefmt
, one useful tip is thatFmt.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.