Obtaining the necessary type for format strings + arguments

I have need of a way to print using any existing printer, with the output stripped of all cuts, linebreaks, and indents (think of presenting messages containing probably-small bits of pretty-printed data to users in confined UI spaces).

I landed on this pattern:

let plain fmt =
  let open Format in
  let b = Buffer.create 64 in
  let ppf = formatter_of_buffer b in
  let fns = pp_get_formatter_out_functions ppf () in
  pp_set_formatter_out_functions ppf {fns with out_newline = ignore; out_indent = ignore};
  fprintf ppf "%t" fmt;
  pp_print_flush ppf ();
  Buffer.contents b

let str = plain @@ Format.dprintf "foo%a" (Iter.pp_seq Int.pp) Int.(0 -- 100)
....

This certainly gets the job done, but I would very much like to have a single function to call as with asprintf. Unfortunately, whatever I try seems to get in the way of the compiler producing the correct type for the format string and its args:

let p2 x = plain @@ Format.dprintf x
let _ = print_endline @@ p2 "%d" 5

Error: This function has type
         (Format.formatter -> unit, Format.formatter, unit,
          Format.formatter -> unit)
         format4 -> string
       It is applied to too many arguments; maybe you forgot a `;'.

I suspect that this is not possible given (my understanding) that the return type of the proximate printing function (p2 here) is what the compiler keys on, but I thought I’d be hopeful and see if there’s any way to get a tidy asprintf-like function with the custom behaviour described above.

2 Likes

Maybe kfprintf can do enough for your case. What about:

let plain fmt =
  let open Format in
  let b = Buffer.create 64 in
  let ppf = formatter_of_buffer b in
  let fns = pp_get_formatter_out_functions ppf () in
  pp_set_formatter_out_functions ppf
    {fns with out_newline= ignore; out_indent= ignore} ;
  kfprintf
    (fun ppf ->
      pp_print_flush ppf () ;
      Buffer.contents b )
    ppf fmt

let str = plain "foo%a" (Iter.pp_seq Int.pp) Int.(0 -- 100)
let boo = plain "boo%i" 42
2 Likes

Hi @cemerick.

I suspect what you want is the kdprintf function, which is similar to your dprintf approach but takes the pieces in the right order to be properly polymorphic in format strings.

let plainer : 'a. ('a, Format.formatter, unit, string) format4 -> 'a =
  fun fmt -> Format.kdprintf plain fmt

This works in the way you expect:

# let () = print_endline @@ plainer "%d" 5;;
5

You’ll need OCaml 4.08 to have access to kdprintf, however.

3 Likes

That’s great Craig, much appreciated. @jjb’s tweak also works, but it seems like having the plain “combinator” around might be useful otherwise in the future.

<rant>
I was an inch or two away from using kdprintf like this, but I seem continually confused by both the signatures and documentation of the various Xprintf functions…something I used to feel badly about, but there are too many cases where Xprintf types just don’t make sense. For example, relevant to kdprintf (and somewhat dprintf):

  1. the signature of kdprintf is ((formatter -> unit) -> 'a) -> ('b, formatter, unit, 'a) format4 -> 'b, but those type parameters overstate the function’s flexibility: 'a and 'b must be the same type, which the compiler will figure out (let plainer fmt = Format.kdprintf plain fmt is a sufficient definition, the existential type turns out to be superfluous). Presumably there is a good reason why there is more than one type parameter in that signature, but it’s not clear to me.
  2. If you give plainer an explicit grounded type:
    let plainer_full : (string, Format.formatter, unit, string) format4 -> string =
      fun fmt -> Format.kdprintf asprintf_fmt fmt
    
    The function compiles without complaint, but format strings will not be inferred:
    Error: This function has type
         (string, Format.formatter, unit, string) format4 -> string
       It is applied to too many arguments; maybe you forgot a `;'.
    
  3. You can provide a completely incoherent combination of types and “continuation” functions, which the compiler will happily oblige:
    let plainer_int fmt : int = Format.kdprintf plain fmt
    
    No type error, even though kdprintf's result depends entirely on the “continuation” function (plain in this case, returning a string).

Some of this might be a consequence of the very special treatment that the compiler provides for format* types, and maybe some is due to legacy of how Format has grown over the years, but the net result is consistently confusing anytime I wander off of well-trodden paths.

FWIW, at least the last of the above issues could be addressed if e.g. kdprintf's signature were tightened to reflect its dependence on the provided continuation function:

module FFormat : sig
  open Format
  val kdprintf :
    ((formatter -> unit) -> 'a) ->
    ('a, formatter, unit, 'a) format4 -> 'a
end = struct
  include Format
end

With this, incoherent types are helpfully rejected:

let plainer_int fmt : int = FFormat.kdprintf plain fmt
Error: This expression has type string but an expression was expected of type int

But alas, format strings are no longer inferred properly:

let plainer2 fmt = FFormat.kdprintf plain fmt
let s = plainer2 "%d" 5
Error: This function has type
       (string, Format.formatter, unit, string) format4 -> string
       It is applied to too many arguments; maybe you forgot a `;'.

:upside_down_face:

I will probably eventually dig into Gabriel Scherer’s post on the format types and how format string inference works, but I don’t think in this case understanding how and why things work as they do will make practical non-garden-path usage of these functions any easier.
</rant>

So, there are lot’s of things to untangle here. In general, I agree, the documentation is not right. The fundamental reason is that as OCaml API designers, we are used to rely on the types to explain how a function might be used, and the documentation only has to explain what it does.
As your rightfully point out, for the Xprintf family of function, the types are utterly useless at explaining how they can be used.

Here, the important piece you are missing is that 'b and 'a play very different roles in ('b, formatter, unit, 'a) format4.
'a and 'b are equal If and only If you use a formatting string with exactly 0 hole. Otherwise, they are different.
In general,

  • 'a is “the return type”. It contains the type returned by Xprintf after application of all the arguments. That’s why it’s set to string for sprintf, and unit for fprintf.
  • 'b is “the arrow type”. It is of the form foo -> bar -> baz ..... -> 'a. It should never be constrained by the Xprintf function, as that would limit the accepted format strings.

Unlike what you said in point 1), 'a and 'b can (and generally are) different when using kdprintf. For instance:

let f fmt = Format.kdprintf (fun f -> f Format.std_formatter) fmt ;;
val f : ('a, Format.formatter, unit, unit) format4 -> 'a
let s : _ format4 = "Foo %s %d bar" ;;
val s : (string -> int -> 'a, 'b, 'c, 'a) format4
# f s "hello" 3

Your “ground” type in 2. is wrong, as it prevents any uses of %X in the format string. Same for the simplified type in your FFormat module.

Finally, you can build valid cases where the the return type is an int, so it can’t be forbidden “à priori”. For instance a function that returns the size of the printed stuff:

let print_and_size = let p1 = position_in_chan () in Format.kfprintf (fun _ -> let p2 = position_in_chat () in p2 - p1) my_chan  ;;
val print_and_size : ('a, Format.formatter, unit, int) format4 -> 'a

While you might not need to understand “how format string inference works”, it’s useful to have an idea of what those parameters mean, which is exactly what Gache’s blog post explains.

In general, it’s difficult to really change/improve the API without preventing valid and practical usage of format. The interplay of the various parameters is indeed a bit complex, but has been proven to lend itself to lot’s of nice uses.
What is possible, and that we should definitely try to do, is to improve the documentation to make it more accessible, and less of “ask Octachron/JJB/one of the gabriels for magic runes on discuss”.

3 Likes

These sorts of topics always make me feel like I’m surrounded by trapdoors where I can be proven to understand nothing. :slight_smile:

Given the existential type plainer : 'a. ('a, Format.formatter, unit, string) format4 -> 'a, where the concrete instance clearly returns string, how is the ground type not (string, formatter, unit, string) format4 -> string? How would anything else unify?

In this case, the fault does not lie in your understanding, but in the (lack of) documentation. :wink:

When used in a type signature, this syntax is a universal (i.e., it forces a function to be fully polymorphic), not an existential. In this particular case, you can just remove it. Inference already works well.