Ocaml stdlib and death by a thousand papercuts

In OCaml the equivalent is to write a function with the signature:

module M : sig
  type t 
  val pp : Format.formatter -> t -> unit 
end

Such a signature can be used for example in the REPL to print your values by doing:

# #install_printer M.pp;;

Or to print your values with a:

Format.printf "%a@." M.pp v

With an appropriate library like Fmt these pp values are reasonably quick and concise to write manually. Don’t bother too much with the ppx stuff, it’s a rather horrific extra-linguisitic portion of the eco-system which would better off being ignored by newcomers, learning both a language and the build system setup, the logic, the annotation toggles and generation results of a bunch of ad-hoc preprocessors is not such a good idea. Your code will be much easier to maintain and understand if you steer clear from it.

I don’t know where you are in your OCaml journey but is this perhaps simply just because you have a familiarity with Go that you don’t have with OCaml ? I’m pretty sure that if I had to write things in Go myself (which I only read but never wrote) I would also feel that I’m wasting time on trivial things and would long for OCaml where everything feels smooth… to me :–)

10 Likes

Does the why matter all that much? For many newcomers this is a significant hurdle, and it seems entirely unnecessary. The Reason community acknowledged this and created @reason-native/console as a best-effort printer of any value to address this. It’s of course severely limited, but good enough for the kind of basic debugging that most newcomers need. And it could easily be ported to plain OCaml.

2 Likes

Feelings are important :–)

Ah yes there has been a bunch of these in OCaml aswell much before reason saw the light :–) It remains rather limited. Also I think @let-def had something slightly more sophisticated here but I don’t remember the details.

I doubt. The “good enough” here basically puts newcomers in the position of having to understand the representation of OCaml values, which is perhaps not the first thing you want them to learn. I suspect that it is more productive for them to printf the portion they are interested in seing rather than try to decipher a dump of the representation of OCaml values.

In any case for me generic printing is a bit of a red herring. I would rather like to see improvements at the level of “push button” source level debugging. When you have the type information of the values you are inspecting quite a few things becomes possible without having to bother with printf.

1 Like

There’s a reason Golang can provide this facility so easily: it’s a monomorphic language. That means that the -actual- type of every expression can be deduced at compile-time. OCaml isn’t: OCaml’s -type-erasure-based- parametric polymorphism means that you can’t deduce those actual types. The last time I checked, Golang’s proposal for adding parametric polymorphism explicitly eschewed type-erasure.

A better comparison would be to Rust: there, you also can get your version of printing, but it’s via traits, aka typeclasses. Again, Rust has a less-powerful type system than OCaml’s, but from what I understand, it would/should be possible to add them (aka “modular implicits”) to OCaml.

There has always been a tension in programming language design, between explicitness and implicitness. Historically, ML languages have erred towards explicitness, using various forms of inference to make explicit what was not written. That is the meaning of “principal type schemes” after all: “you didn’t write it down, but if you had, there is only one thing you -could- have written, so we inferred it for you.” In this vein, wouldn’t automatic generation of pretty-printers via PPX deriving.{pp,show}, combined with Fmt get you most of what you want ?

2 Likes

Documented here: GitHub - ocaml-ppx/ppx_deriving: Type-driven code generation for OCaml

Build system config here: GitHub - ocaml-ppx/ppx_deriving: Type-driven code generation for OCaml (normally it would be a one-liner change in the dune file)

If you have any feedback or ideas for improving the documentation, package maintainers in my experience are very receptive. So are the OCaml website maintainers: OCaml Cookbook

2 Likes

There’s another sort-of-design-paradigm from OCaml that shows up here, and that I subscribe to:

The base language and standard library doesn’t make lots of choices that would preclude implementers from exploring variety in their packages. What I mean concretely, is that (for instance) there’s the standard PPX infrastructure, and (haha) I maintain a completely independent PPX infrastructure based on Camlp5. And neither of these two has to jump thru hoops: OCaml doesn’t privilege one particular infrastructure. The same is true for build-systems: sure, dune is the pretty-much-official build system for OCaml, but I use Makefiles and barely know how dune works, and I don’t suffer at all.

I think this is a good thing, not a bad thing, b/c it means that people can explore alternatives. I remember when ocamlbuild was the official build system. Then it was oasis. Now it’s dune. Maybe in a few years it’ll be something else (bazel? grin)

3 Likes

I don’t think it’s a red herring. I would like to see that as well, I just don’t see it happening all that soon. Meanwhile, best-effort printers can be implemented in user-space with little effort and no significant trade-offs as far as I can see. It can address the problem immediately, and already exists in dark corners of the ecosystem. it just needs better discoverability. Don’t let perfect be the enemy of good.

The OCaml Cookbook link I gave earlier is looking for a submission for debug printing values. Any submission there will be signal boosted on the OCaml website itself. Great opportunity to come out of dark corners and into the light.

1 Like

I don’t, feel free to advertise that “solution” and give us a nice package that does so.

But personally I just don’t think it addresses the problem. If it did really help, perhaps it wouldn’t live in “dark corners” of the ecosystem… Again, well placed printf statements is generally more productive than trying to decipher Obj dumps, especially for newcomers.

I think it lives in the dark corners because what exists still requires jumping through significant hoops. Let-def’s ocaml-introspect, for example, has no documentation and isn’t published on opam. Something called genprint is published on opam and decently documented, but requires understanding package systems, build systems and PPXs. And at that point you might as well just go with deriving.

To really matter, it needs to be available out-of-the-box.

1 Like

There are quite a lot of tools for programming in OCaml that really matter which are not available out-of-the-box.

Only proven things get integrated upstream. If you really think it’s worth it then your first step is:

perhaps in a few years you’ll be able to convince people that Obj dumps are useful and should be part of the stdlib.

OCaml has made certain design decisions, and those design decisions aren’t easy to work around for some simple functionality (such as displaying data structures). Most GCed languages have a lot more typing information available to them in the runtime, and this allows them to do this kind of thing more easily.

So OCaml’s design decisions have placed it roughly where C++, Rust and Haskell are in terms of printing data structures. C++ at least has a proper debugger with the ability to view at least stdlib data structures – OCaml doesn’t quite have that either.

The other runtime-poor languages such as Rust and Haskell also use this path of macros and derivation (essentially ppx), and to me, this makes sense as the approach OCaml should take as well. It’s just a little bit clunkier than one would like due to the need to activate ppx support both in dune and in some data structures. It’s also another concept for newbies to learn, whereas if ppx infrastructure was integrated into the distribution, it would ‘just work’. It’s worth mentioning that this is probably related to the fact that OCaml lacks a proper macro sublanguage, whereas both Rust and Haskell have it integrated in some way.

2 Likes

I think you’ve put your finger on the underlying reason that many of these papercuts persist. Catering to newcomers requires a focused effort to remove the hurdles they encounter. And if that process requires those same users to go through a gauntlet of hurdles in order to “prove” that a solution is viable, then obviously that’s not going to happen. It’s a self-defeating process.

2 Likes

I don’t quite follow your reasoning. No one said the “same users” had to prove anything. Besides there’s nothing specific to newcomers here. Any OCaml programmer would take a convincing solution to improve printf debugging.

There is no self-defeating process: any large programming language out there has more or less formal procedures to improve the core that involve convincing the language’s maintainers of the worthyness of additions. That’s just basic quality control.

You said “I know something that works”, then convince fellow OCaml programmers and upstream that it does by providing a package with an excellent upstreamable-ready implementation of what you suggest (or try to integrate it to base or containers or extlib or what not to gather user data). You could also try to directly PR upstream but it may be a harder sell at that point.

If you want to improve the eco-system along what you think works, then, unusprisingly, nothing is going to happen by writing messages on this forum.

Complaining by non-newcomers without action, that is the self-defeating process, and the reason why these papercuts persist.

1 Like

It is pretty simple in OCaml as well: if you want this primarily for debugging, a Printf.fprintf style printer may be enough without invoking Format style pretty printers. First you can have a generic printer function for use of Printf formatted text, which requires only a show function to be available for the type in question:

let print_of_show (show:'a -> string) (ch:out_channel) (t:'a) =
  output_string ch (show t)

Then you can provide a printer for, say, a simple pair of integers:

module IntPair : sig
  type t
  val make : int -> int -> t
  val show : t -> string
  val print : out_channel -> t -> unit
end = struct
  type t = (int * int)
  let make a b = (a,b)
  let show t =
    String.concat "" [ "(" ;
                       string_of_int (fst t) ;
                       " , " ;
                       string_of_int (snd t) ;
                       ")"]
  let print = print_of_show show
end

And then use it like this:

let () =
  let pr = IntPair.make 10 25 in
  Printf.printf "Pair is: '%a'\n" IntPair.print pr

I think this would be a sufficient solution if ever Stdlib module had such a function. I know there is a circular dependency issue with that, but in that case I think its worth it.

Note that most of these functions can be found here in the Format module. Nowadays you can easily devise formatter for your monomorphic options, results , arrays, lists with the stdlib.

Set, Map and Hashtbl are missing for which you can use the all round Format.pp_print_iter and (unfortunately not in the stdlib yet) Fmt.iter_bindings.

For example:

module Mset = 
   include Set.Make (M)
   let pp = Format.pp_print_iter iter M.pp
end

Yeah I know you have these functions in the format module, but I think its not good enough because its not discoverable enough. Or maybe we just need to advertise them more in this type of discussion.

Sure, not to mention the horribly long-winded naming convention (hence the existence of Fmt and many hidden Fmt modules in my libraries).

Yeah
I also dont like the design of format, I think “universal buffer” and “pretty printer with auto line-breaks” should be separate, and the first one needs to be in the stdlib while the second one not necessarily.