Why doesn't OCaml have typeclasses (or something similar)?

A lot has been said about the absence of typeclasses in OCaml (and it’s sibling F#), but I haven’t been able to find any good resources explaining why the OCaml’s developers felt those are not needed.

I did find F#'s creator’s reasoning against them here, and I assume the same line of reasoning applies to OCaml as well. Still, if someone can expand on the topic here it’d be great.

I get that any form of type-level programming comes with a certain degree of complexity, but I also find it somewhat frustrating that basic things like printing some data for debug purposes require you to either hand-roll some print function or use ppx. The inability to overload operators is also a bit of a pain, although to a smaller extent. That’s why I’m keen to learn more about the thinking of OCaml’s maintainers about the pros/cons/tradeoffs/alternatives of typeclasses (and similar concepts).

I don’t have the impression the the devs did not think such stuff is not needed. It’s been discussed in great detail for a long time. Problem is, it’s very very complex.

See Modular Implicits - #9 by gasche

2 Likes

I am a bit more interested in the foundational reasoning to avoid them in the first place, but it’s nice to see that the maintainers are looking into some potential solutions the problem.

Sadly, it seems the idea of modular implicits didn’t get far. I wonder if this is still in the cards or it got abandoned down the road.

Hello @bbatsov,
I am not an expert but as @yawaramin commented, this topic has been discussed in the community in the context of modular implicits. For example here. But it seems to be not easy to introduce.

On the other side, there were discussed ways to “emulate type-classes” (whatever it means): here or here

1 Like

I guess because OCaml and its ancestors (Caml Light, Caml, ML) more or less predate type classes, or at the very least when OCaml was created type classes were still experimental and unproven. Whereas modules and functors were well-understood.

1 Like

First, I’m an advocate of typeclasses – after using Rust’s traits, I think their value is -immense-. But there is a good and valid argument to be made, that typeclasses can produce hard-to-read-and-understand code, and this is a real problem. An older version of the Google C++ style guide pretty much banned “C++ template metaprogramming” – that is, it said “you can use the templates that are already there, but don’t go writing your own!”

With OCaml today, you can read a bit of code, and if you know where all the names in it come from (and you can figure that out in a straightforward way), then you know what the code does. There are no “hidden names” that you can only get access to if you know how the typechecker works. And that’s -a- giant problem with typeclasses – some of the names (e.g. the full name of the “+” operator in 1. + 2.) [notice that I wrote “+” and not “+.”] are computed by the type inference algorithm.

There is a lot to be said for the position that you should not need to run the typechecker to understand a piece of code.

And I write the above, as an -advocate- of typeclasses.

4 Likes

I would like to emphasize that these two issues are really orthogonal: “printing some data for debug purposes” is about defining new functions, while “overloading operators” is about figuring out which existing functions to use at a given type.

Only the latter is solved by type classes/traits/implicits. For the former, PPX is used today, but it is an imperfect solution (mostly because it is syntactic and separate from the compiler). It corresponds to the “deriving” mechanism found in Haskell or Rust.

Personally, I think that in terms of sheer utility it is the latter, having a built-in deriving mechanism, that is much more useful than having type classes/implicits (which allow overloading). See also this related thread:

Cheers,
Nicolas

2 Likes

I think this is correct.

My memory is a little shaky, but I recall looking at the generated raw code for debug-printing, close to when the Rust compiler gets it (and after macro-expansion). One can see that debug-printing uses the Rust typeclass machinery to generate the per-argument print functions. It makes the actual debug-macro really simple, and more generally Rust gets away with having much, much simpler macro facilities than OCaml, -because- all the type-directed compilation that OCaml needs in its macros (e.g. type-derivers) is taken care of by typeclasses/traits.

This was also the case for the generated marshallers for interfacing Rust with Python.

1 Like

I would like to emphasize that these two issues are really orthogonal: “printing some data for debug purposes” is about defining new functions, while “overloading operators” is about figuring out which existing functions to use at a given type.

I get your point, but there’s also the take that they are similar in the way of establishing common ways (interfaces) for working with data, which was the point I was trying to make. E.g. in Haskell and Rust printing is backed by typeclasses/traits. Admittedly printing could also be handled by some runtime inspection (as F# does it), but from what I’ve gathered that’s not possible with OCaml’s runtime.

Yeah, that’s totally fair and it’s the classic problem without a clear solution - simplicity vs convenience (which usually adds some complexity). For the record - one of the aspects of OCaml that I like is its simplicity (compared to something like Haskell), but when I was playing with F# and Rust I liked the convenience they provide in some cases. I did also read that Rust’s compiler is quite slow, though, which I guess is related to the complexity added by traits and friends.

A point that has surprisingly not made yet is that type classes are anti-modular by nature: they require to choose a way to associate behaviours to a type constructor in an unique and privileged way.

This means that typically in Haskell, the standard library decide for you which group operations, partial operations comparison, and printing function should be used for integers. And if you require other operations, you need to define a new incompatible type to reclaim the right to define type classes on this new type.
This also implies that type classes are not compatible with type abstraction since they require to be able to associate an unique type class to any type in any context, whereas type abstractions means that one cannot assume that all type equalities are visible in a given context.

Also the work on modular implicit is still on-going. For instance, the first step Modular explicits by samsa1 · Pull Request #13275 · ocaml/ocaml · GitHub is under review, and I still have some hope that it will be ready in time for OCaml 5.5 .

16 Likes

More details can be found about these points in this paper in particular in section 4.2.

6 Likes

My impression is that the type inference is not hugely powerful in Rust, which means that you often have to write something like:

let myvar: List<MyType> = f ();
myvar.print()

Where in OCaml you could write:

let myvar = f () in
List.pp MyVar.pp myvar

or

let myvar = f () in
pp (module List.P(MyVar)) myvar

Notice you have to specify the type in the function call instead of the variable declaration, but you write it the same number of times.

I think this could be done right now with changes to the stdlib/a replacement stdlib, no need for new language features.
For more complex stuff like arithmetic or monads where infix syntax is needed and there are multiple operator this would get old quickly, but the situation could be improved with library changes.

Of course for cases in rust where you don’t have to specify the type, or for more complex cases in OCaml where you have 'a running around then the differences are very big, but I think we could really improve the situation with just library stuff.

Then modular explicit would also help a lot, but this would also need a stdlib properly designed for it.

3 Likes

Which leads to the design of modular implicits, where you can bring other definitions into scope. This design I’ve critiqued earlier, based on experience with Scala (though Scala’s implicits is a bit different from modular implicits in its details).

I still see the resulting problem of there being invisible ‘holes’ in the code to be filled out as a big problem for readability (even though several instances can’t be in scope at the same time) - which goes directly against what (that has also been mentioned here) I see as a major advantage of OCaml over other languages; simplicity and readability.

As has also been mentioned in other threads (and earlier in this thread) - it’s possible to get several of the niceties of implicits via other features.

OCaml should not try to copy other hyped languages just to copy them; keep the identity and advantages of the language in sight.

3 Likes

No argument from me.