OCaml real pain points

This blog post got some views in HN lately.

There is not much value in it, but it got me thinking: I have my own list of pain points with the language, I wonder what others lists look like.

So, here is mine. It will readily give away that I’m using the language for very boring, let’s say pragmatic, tasks and am not a heavy user of sophisticated features. But I might not be the only one.

  1. First and formost: The compiler is not smart enough to infer record types from other modules. I end up explicitely specifying the types of most function arguments that are records so that I can access their fields, even when the compiler should know the type already (for instance: List.iter (fun r -> r.field) rs won’t work even if the compiler is able to tell that rs is a list of SomeMod.record.

  2. Match guards are global. No way to write: match x with Foo | (Bar x when x > 0) -> ...

  3. Order of definitions for compilation units → not always trivial to find a decomposition that is a tree (recursive modules help only within a single compilation unit, and mandate explicit signatures)

  4. Syntax: no nice preprocessor, not lispy enough.

  5. No anonymous records or anonymous sum types (hey, this one has evolved recently! \o/)

  6. Memory layout: impossible to unbox things (also, this might be slowly evolving?)

  7. No way to ensure a data type is immutable.

  8. No way to assert all possible non-runtime exceptions are caught (getting there someday?)

  9. To copy a record type, nothing shorter than the slightly embarassing let copy = { record with wtv = record.wtv } which won’t deep-copy anyway.

  10. Despite raise being a keyword not a function, raise Failure "bummer" is parsed like an unary function, and therefore does not compile.

    Also very curious how others cope with those.

4 Likes

My main pain point is that I never remember the order of arguments in List.fold_left and its functional argument.

6 Likes

Just two remarks:

  • Anonymous sum types: polymorphic variants
  • Anonymous records: labelled tuples (introduced in 5.4)

raise is an ordinary function, defined in Stdlib:

Cheers,
Nicolas

3 Likes

You are right, I wonder where I got this idea that it was a keyword from. Maybe just syntax highlighting. I will henceforth add those parenthesis with no grievance :slight_smile:

I used to have the same issue years ago, but then @mjambon told me this: “it’s easy to remember, _left and _right is the position of the accumulator”!

7 Likes

One of my big pain points is repeating module types and types in MLI and ML files. I use module types a lot, and they quickly get very long, especially with documentation. Doing the same change (in the same place, since order of values matters in a module type) is quickly annoying.

There are some tricks to work around this (moving the signatures to a new standalone ML or MLI file) but I would really appreciate a syntax to import from my own MLI. I’ve read somewhere the ppx_import allowed this but I could never get it to work…

Another pain point is the lack of typeclasses/overloading, especially for print functions. Hopefully the work on modular implicits will eventually fill that gap.

A third pain point revolves around module type errors. They get very verbose very quickly and do not do a good job at pointing you to the actual problem, especially with the LSP integration (Oh, you flipped the order of one function’s arguments? let me highlight the whole 1000 LoC+ module as erroneous for you…)

8 Likes

Modular implicit would not really help in my opinion. Modular implicit allows to select a function automatically, but the problem here is not the the functions are too hard to select, its that they are stashed away with a weird name in the format module and probably are too powerful. (Format.pp_print_option require a formatter for the None case, or prints nothing by default which is not what you want when debugging).

My point is that having Option.pp and other such functions without modular implicit would get you 80% to “printing is nice” but having modular implicit and no improvement to the stdlib would get you 0% there because you still need the printing functions which are not available.

So we should stop waiting for modular implicit and make an improvement to the stdlib. Its not easy work either to come up with a good design but probably way easier than completing modular implicits.

1 Like

For print functions, we have ppx_deriving with deriving show. This is the closest thing to Rust’s print macros. It is perhaps unfortunate that the ppx is not automatically applied, but setup is fairly trivial.

A lot of people use fmt precisely because it gives you these nice combinators for Format.

4 Likes

I know this exists but it does not allow you to do Option.pp String.pp my_opt because these functions don’t exists. And while printing a new type is useful, I think having these would already help quite a bit.

Maybe it has some nice-ish way of doing that because [@@deriving show] does not fail when applied to a type with an option in it, but why would you need to know about a deriving ppx when you just want to print some value of a type you did not define ?

I would be perfectly happy if the Stdlib included the output of [@@deriving show] in every module, but I think that might not be possible because you don’t want the List module to depend on Format.

Another angle to the printf debugging problem is of course push button source level debugging.

2 Likes

I agree it’d be nice but in the meantime Fmt.(Dump.option string) my_opt works just as well. The main issue is having to remember the incantation for each type, it is particularly difficult for newcomers. 5mn ago I had to write the correct incantation of “Fmt combinators for a list of pairs where the first element is an option” for a colleague who has a lot of experience with OCaml but hasn’t used it for a long time… It’s not Fmt’s fault, it’s just that one shouldn’t have to write that in the first place

4 Likes
  • Value constructors could behave like functions; too often I write let foo x y = Foo (x,y) to make value construction more convenient.
  • Functions take parameters on the right but type constructors take parameters on the left. It bugs me mostly on an aesthetic level.
  • Contributing to the Opam repository for the common case involves too many steps.
  • The ecosystem relies on evolution to the point that a lot of effort is abandoned. It’s fine for the standard library to primarily support building the compiler but there is no accepted extended standard library. Much younger languages have stronger standard libraries.
  • No (good) debugger support for native code.
  • A plethora of profiling solutions, all slightly awkward to get going.
3 Likes

The real pain point is, when I took a class a couple years back in OCaml, nobody seems to know and care about ppx, writing verbose code with less than optimal experience… Then people stood on the bad experience and stopped caring about OCaml and started complaining

I have a similar complaint, in that I would really appreciate a syntax that creates a default MLI from my ML, allowing me to say in my ML “in the MLI, declare/do not declare this, or declare this with this type”, similar to the way the keywords “private/public” work for class methods in OO languages. As my philosophy is “make the majority of modules simple and self-contained”, this would relieve me of the burden of writing an MLI that is mostly redundant with the ML, for most of my modules.

2 Likes

On the contrary, I believe modular implicits would help a lot. They automate menial tasks I don’t want to think about. The compiler can select the relevant print function for me, gaining me time. Yes I can do the same thing manually and yes, there could be a better API for doing so, but if I can avoid it, that’s even better.

Although I’m not sure if modular implicits can chain the way typeclasses do, i.e. given en String.Printerand a functor Option.Printer, derive the StringOption.Printer automatically.

I kind of want the reverse: I want to write something in the .ml to say that the type definition is “as per the .mli”.

type t = [@mli]
module type S = [@mli]

(but a better syntax)

4 Likes

Personally, I often dream of a terser solution, of having a print_anything:'a->unit function that gives a default printer for any type, including user-defined ones, and also a customize_printer: type_descriptor -> ('a->unit) -> unit value for the cases when the user is not satisfied with the default. Currently, the #install_printer and #remove_printer in the toplevel are imperfect substitutes for this.

What I meant is not that modular implicit are useless, but that they are useless by themselves. If Option.Printer does not exist, having modular implicit would not help you at all. The relevant print function needs to be written before it can be selected by modular implicits, and that is something that could be done right now to improve the situation quite a bit.

I think modular implicit are meant to be able to chain like you say. At least thats what I remember from this talk at ICFP. Not sure if a paper is available.

2 Likes

I totally agree, I don’t count the number of hours spent on torturing my brain to try to find a good organization of ml /mli/ intf files (including a good documentation with examples and images)… Does this have to be that hard?

1 Like