Login

Format strings

Format strings are magical in OCaml: if a string has the right type, then the language turns it into some structured data including a function over the typed arguments that the format string requires:

# ("%d %s %d": ('a, 'b, 'c, 'd, 'e, 'f) format6);;
- : (int -> string -> int -> 'f, 'b, 'c, 'e, 'e, 'f) format6 =
CamlinternalFormatBasics.Format
 (CamlinternalFormatBasics.Int (CamlinternalFormatBasics.Int_d,
   CamlinternalFormatBasics.No_padding, CamlinternalFormatBasics.No_precision,
   CamlinternalFormatBasics.Char_literal (' ',
    CamlinternalFormatBasics.String (CamlinternalFormatBasics.No_padding,
     CamlinternalFormatBasics.Char_literal (' ',
      CamlinternalFormatBasics.Int (CamlinternalFormatBasics.Int_d,
       CamlinternalFormatBasics.No_padding,
       CamlinternalFormatBasics.No_precision,
       CamlinternalFormatBasics.End_of_format))))),
 "%d %s %d")

You can get this without reconstructing that type from Stdlib's ^^ and format_of_string.

If you're willing to work with types with suspicious names like Camlinternal, you technically can:

# match format_of_string "hi %d" with CamlinternalFormatBasics.Format (CamlinternalFormatBasics.String_literal (str, _), _) -> str | _ -> "???";;
- : string = "hi "

But realistically the upside here is just that you can store format strings in variables:

let f x n =
  let fmt =
    format_of_string
      (match x with
      | `En -> "Example number %d"
      | `Ru -> "Пример №%d")
  in
  Printf.sprintf fmt n

let () =
  assert (f `En 20 = "Example number 20");
  assert (f `Ru 47 = "Пример №47")

Less-magical varargs can be simulated with higher order functions, example adapted from stackoverflow:

module Print = struct
  let print f = f ""
  let s y x g = g (x ^ y)
  let i y x g = g (x ^ string_of_int y)
  let endl a = print_endline a
end

let () = Print.(print (s "Hello") (s ", ") (s "world!") endl)
let () = Print.(print (s "A number: ") (i 100) endl)

Or with a preprocessor, example using ppx_format which does this:

let target = "world"
let count = 100
let () = Printf.printf {%i|Hello, {%s target} x {%d count}!{%s "\n"}|}

You can also accept runtime format strings that match a provided format string's type:

let () =
  let fmt = Scanf.format_from_string Sys.argv.(1) "%s%d" in
  Printf.printf (fmt ^^ "\n") "star" 3

As used:

$ ocaml o128.ml "%ss? I have %d of them."
stars? I have 3 of them.
$ ocaml o128.ml "%s count: %d"
star count: 3
$ ocaml o128.ml "%s invalid"
Exception:
Stdlib.Scanf.Scan_failure
 "bad input: format type mismatch between \"%s invalid\" and \"%s%d\"".