Printf / ANSITerminal pain points

Another pain point which hasn’t been mentioned yet (or maybe it fell through the cracks of this rather long thread) is the composition (or rather, lack thereof) of printf functions.

Concrete example:
The ANSITerminal exports

  • a val printf : style list -> ('a, unit, string, unit) format4 -> 'a function
  • a eprintf variant with the same type
  • a val sprintf : style list -> ('a, unit, string) format -> 'a function

First, the format4, format, format6 types are arcane because of the positional type parameters. There could be a single object type to hold the type parameters and it’d give names to them which would be more readable.

Second, if you want to print into a buffer well you can’t do that directly (or can you? I’m not 100% because it’s such a complex piece of the stdlib), you have to go through the string and then add that stirng to the buffer. You have to make the code more complex and less efficient.

Now one could patch ANSITerminal to have a bprintf variant. And then also a ifprintf and ibprintf and a plethora of others. But it’s a shame that libraries have to export half a dozen functions for essentially the same functionality over slightly different data types.

I think there’s a good opportunity for language/stdlib design there.

2 Likes

If ANSITerminal had provided fprintf, even instead of the other 3, then it could be used to print to stdout (using Format.std_formatter), stderr (using Format.err_formatter), a buffer of your choice (using Format.formatter_of_buffer), a string (using Format.str_formatter), etc. I think this is more a case of a library exposing the convenience wrappers but not the general form of an operation, rather than an issue with the standard library’s expressiveness.

But yes, format6 is arcane. Perhaps an object type would make it more understandable. Sounds like something to trial with a small wrapper library of Format adding and using such a type alias.

I’m rather excited for the l*printf functions (New `Format` and `Printf` `printf`-like functions that accept a heterogeneous list as arguments by zazedd · Pull Request #13372 · ocaml/ocaml · GitHub), which appear to make composing print functions much easier, without the need for continuation based code

5 Likes

oh that is a welcome addition! thanks for the pointer

The real answer is, and always has been imho, that people ought to be able to build their own out_channels. This way we’d only need fprintf because bprintf would just be fprintf into a buffer-backed out_channel. This is really a place where the stdlib lacks extensibility.

3 Likes

I don’t see how you can make a buffer into an out_channel so you definitely need Format.fprintf rather than Printf.fprintf. But when you print out terminal control characters they shouldn’t count towards the length of the pretty-printing box thingy features of Format. The simplest thing (the thing that’s not huge amount of code) is to only expose Printf-like extrapolation strings.

Oh and also surely you mean kfprintf because with fprintf alone you can’t do something like

let warn m =
  kfprintf
    (fun _ -> some other code here)
    style
    msg

oh and ikfprintf as well! (although I guess you could get away with passing a devnull fmt but you’d still pay execution time though)

So I don’t think “just expose Format.fprintf-compatible functions” is a workable solution.

These kind of feel more like pain points of ANSITerminal specifically. Basing it on Format instead of Printf would be more extensible, and control characters don’t have to count towards the box length, since Format allows you to control the length of any printed string (e.g. as 0). Even without using Format, the stdlib’s Printf includes functions like bprintf, which I assume ANSITerminal could expose too. (Although I say this as someone who hasn’t used ANSITerminal or much less contributed to it, so it may be more complicated than that.)

I do agree with the more general pain point that the ecosystem’s status-quo makes composable APIs much harder to write, though, which leads to situations like this. A better design around channels (like letting us build our own) would give users a lot of these desired benefits for “free.”

ANSITerminal was picked randomly as an example.

I also have issues with CCUnix where the call* functions can’t easily be wrapped/combined because they have the interpolation arguments. You end up having to do normal printfs and then pass the string as a whole or something.

I think the fact that there are two separate modules in the Stdlib as well as two prominent libraries with the single focus of providing good alternatives to the Stdlib (Fmt and Pprint) goes to show that things could be improved.

I hit the same issue recently and complained about it in some other thread, where some of the maintainers proved useful explanations in response. Among other things, if a library function that accepts formats doesn’t provide a continuation variant, wrapping it with:

Format.kdprintf (fun ppf -> do_thing @@ the_library_function "%t" ppf)

seems to be blessed way, and the soon-to-come heterogeneous list approach to format arguments will allow you to just write it in the natural way.

could be nice to define a let@ to wrap around the continuation

let (let@) x f = f x
let f x y =
  let@ s = Format.ksprintf Fun.id "%d%d" x y in
  String.length s

I’m sure similar variants can work for different k*printf functions

1 Like