Printing, modular implicits and the Stdlib

I wanted to share thoughts I had on the thorny “printing question” with the community. I have written a small blog post here, and I would enjoy discussing the questions raised here.

3 Likes

I think it’s worth asking why we want to print. Is it mostly for debugging? Because anything else can’t be automated in most cases. I have started to provide conversion to JSON for my types. This can be pretty-printed and as such takes care of formatting output for debugging. I’m not using it, but PPX for conversion to JSON exists and could take care of most work and does not require modular implicits.

I am not arguing for automating printing for new types, but for printers for the existing types of the stdlib. Such printers are very easy to write, I do not think automation is that useful for them.

PPX are indeed the right solution for automating conversion to json , but you do not have json converters in the stdlib.
Although having such json converters could be a way to solve the problem I talk about, it would not be accepted, because some people think that tying ocaml to a serializing protocol is a bad idea for the future. I believe simpler printing functions would not receive such backlash.

I read your note, and my first thought was: have you looked at Rust’s macros, and how they can be used to define debug and pretty-printers ? That might be useful, b/c they combine simple macros, with traits (== modular implicits) to do what ppx_show does, only much more simply. I’ve seen traits used elsewhere in Rust, too: in the Rust<->Python interface, traits are used to somewhat automagically generate the marshalling code to convert Rust values into Python values (and for generating function stubs).

Rust’s traits are quite powerful, and they allow for Rust to get by with a very, very weak macro system. Hopefully when OCaml has modular implicits, they’ll be sufficiently like Rust’s traits, that they can be used in similar ways.

(* Such constraints are not possible in OCaml for now *)
let print (M : Printable) (v : M.t) out_channel =
M.print v out_channel

AFAIK, this is already possible in OCaml today with first class modules and a slightly more verbose type annotation.

The only thing that would be new here with modular implícits is that M would be inferred automatically.

I am not completely sure. I know it does not work with this kind of annotation. Anyhow the syntax would be too annoying, because its really print (module M) v which is worse than M.print v in my opinion

I have used rust macros but only briefly, and I do not really understand their power. Maybe modular implicits will be just as good as macros + traits, but I am not really concerned with that. I do not think that generating printers is the biggest issue we have with printers. I think the do-it-yourself or ppx approach is fine for when you define a new type.
The biggest issue I have is that the stdlib does not have printers. A beginner having written a nice little module would not even have a convention to follow on how to add printers to it, except the one given by ppx which are not official and an extra dependency.

1 Like

More complex types like 'a list do not have such functions at all.

Don’t we have Format.pp_print_list ? And more recently Format.pp_print_array - and even Format.pp_print_iter.

Oh indeed. I was sure I had checked, I have messed up here. Although it does not really do what I think it should (that is print a list like it is a list literal, as in the toplevel)

FWIW there is a convention spelled out in fmt and the Fmt.Dump provides literal formatter for those stdlib types that afford it.

It’s not in the stdlib but it’s used by many as far as I know.

3 Likes

Yep, I just think there should be one in the stdlib, and maybe even one that does not use format.
Format seems to to be intented to be for general pretty printing, and not for simple and stupid debugging.
I think the fact that the functions in both Fmt and Stdlib.Format to print stdlib types do not print the types as you input them, but are more combinators to be able to print data in the specific way you desire shows this. Its fine that Fmt does this, but its not what I think List.print should do, which would be something like the toplevel does.

Well the toplevel uses Format to do its job, so the convention I point to is actually the one you want.

No its not what I want. The naming convention is fine, its just that it does not print what it should if it was in the List module. The toplevel does not give you a choice on how to print lists, and I believe that List.print/pp should not either.
In batteries, (Bat)List.print does something like that, except you have the option to change it, which I am not sure is really useful. (It is useful that Format/Fmt provide the functions they do however, I just think this kind of combinator is fine in its own module)

That in the convention spelled above would be pp_dump (which is so so, maybe pp_debug or pp_literal would be better) as implemented by Fmt.Dump.list.

But in any case you always need to give the choice on how to print the polymorphic type you cannot recover this information at runtime.

I’m not exactly sure what you really want here.

If people simply provide pp and/or pp_dump functions for all their types then printf debugging becomes a few combinators application away.

So without the help of more sophisticated linguistic support I’d suggest that’s this is the best we can have for now.

(Or simply expose typegists)

Yes pp_dumb is fine, maybe pp_debug would be better. I just do not think that it would that useful to have List.pp, but that having List.pp_dump is very important.

But in any case you always need to give the choice on how to print the polymorphic type you cannot recover this information at runtime.

Yes and that’s fine.

There may be an issue with circular dependencies that prevents putting pp/pp_list/anything format in the stdlib modules though

(Or simply expose typegists)

Yeah, I think this way harder to get people to agree to, so probably sticking to simpler printing is the way. Although it would be nice to have this

Here is how to express such a function:

let print 
  (type a)
  (module M : Printable with type t = a)
  (v : a) = ….
3 Likes

In my spare time I’ve been writing a ‘modified modular implicits’ proposal where I suggest sticking with this exact current syntax and calling convention, and introducing only one new, lightweight, piece of syntax at the callsite: print _ my_val, where the _ character means ‘summon a first-class module of the correct type here’. And implicit resolution would search through all modules in scope to find the correct one, instead of needing to mark modules as implicit. This would require minimal (or zero) code changes in userland libraries.

Would probably be a bit tricky to implement the resolution though since the search space would be much bigger.

1 Like

Will that work when you need to compose modules (with functors) to generate the desired module? B/c that’s a common thing to need to do.

Yeah, that’s a complication but it’s definitely a known factor. In Scala it’s called ‘implicit derivation’ i.e. deriving the implicit you need from other implicits in scope. In my proposal in OCaml e.g. we should be able to apply Map.Make(String), internally assign it to a name, and pass it as a FCM for the resolution of a _ which wants a parameter (module M : Map.S with type key = string) because both Map.Make and String are in scope.

As a simplifying assumption (which the modular implicits paper also makes), we could specify that only modules directly in scope and reachable without projection will be considered in implicit resolution. So we’d have to actually give a hint to the compiler by having module MapMake = Map.Make in scope.

1 Like

That sounds a bit like the auto type from c++. I think that having submodules for modular implicits has an advandage : the List.Print module can be a functor, even though List is not. If you do not have that, I dont know how implicit print is gonna work with types that take parameters. (Maybe we could have both, but then its a bit confusing isnt it ?)