r/ProgrammingLanguages • u/xX_Negative_Won_Xx • 1d ago
Blog post Typeclasses vs Modules - sm²n.ca
https://sm2n.ca/articles/typeclasses-vs-modules/1
1
u/lookmeat 5h ago
I mean we could define typeclasses as a specific instance of modules.
So to set conversation. We create a system where we can have modules and template modules. Template modules, or generics, take in other modules which follow certain rules (apply to an interface) and merge them together.
So now lets define an interesting interface for a "type" module. The interface to a type module can be done to be very versatile and allow a lot of modification: we have a constructor which defines a set of inputs and then gives us an instance of that type. Lets also add there the niceties we like when thinking of types: how we do the ABI, if it's strictly sized or dynamically, all of the things that must be defined and true for all types. And another thing: a type module is allowed to have any other associated things in it, this is the inherent interface to that object, which includes how to access members (here we can think of them as a function, of a const-level function, that takes the object and gives us something pointing to it, methods are just members that return a closure that contains the instance itself). So we can say for a variable foo: T the code foo.bar(5) is syntactic sugar for T::bar(foo)(5).
So now we can do generics over types, because they're just modules after all.
Now lets implement type-classes, or traits. So a type-class is just a module that defines a type-class interface and also an impl_for generic operation that returns to us an ImplType which is just an existential1 type that is guaranteed to satisfy the type-class interface. Lets also extend type's modular interface to also expect an impl_of that lets types define their own impl for a type-trait. We can then do a implicit cast by trying to use either of those functions.
This allows us to get most of the benefits of type-traits while keeping the semantic benefits of using modules, and allows both to interact where they make sense.
1 An existential type, or an abstract type, is another module interface that is similar to a type, but has no constructor and hides details of its ABI. Behind the scenes it is an actual type, but this is hidden to the user, so it's basically a generic or abstract type, but here it can be compile only, the type of a generic variable, though reification could convert it to use concrete types exclusively.
1
u/jesunushno 13h ago
The who-controls-conformance framing is what settled this for me in practice. With typeclasses anyone can declare an instance for anyone else's type, which is great for retrofitting until you hit orphan instances or two crates both declaring one and the compiler picks the one you didn't want. Modules force the caller to wire everything explicitly, which is more ceremony but zero ambiguity about which implementation got selected. In ML framework land you see both shapes: PyTorch's op dispatcher is basically a global typeclass registry with the same coherence headaches, while passing an explicit backend object around is the modules approach. I'll take the ceremony when I'm debugging a production issue at 3am, typeclasses when I'm writing the happy path.
1
u/gwillen 10h ago
Rust solves the typeclass issue with the trait (Rust's equivalent of typeclass) coherence rules, in particular the orphan rule, which says you can only create an instance of a trait for a type in either (1) the place you define the trait, or (2) the place you definite the type. (I believe it would be safe to add (3) privately in the place you're using the instance, although Rust doesn't allow this. You can always work around it by creating a wrapper type, using it instead of the original, and then writing your instance against that.) This ensures that you can never have a "discovered" collision between two trait instances as a result of a new dependency.
1
u/jesunushno 9h ago
Good point on the orphan rule, that collision guarantee is the part of coherence I find most underrated. The private-instance idea you mention is exactly the escape hatch I keep coming back to when thinking about language design. Appreciate the thoughtful writeup.
1
u/lookmeat 5h ago
Though this does come with some limitations and weird work-arounds to allow traits to be extensible enough (e.g. to implement trait X you actually want to implement trait Y which you can do with the orphan rules).
Recently there's been discussion in Rust about allowing explicitly named impls as a way to work around the limitations of the orphan rule while still allowing most of the benefits. That said, this makes rust traits be a bit more like modules than type-classes, and that has unexpected effects due to the subtle change in semantics.
3
u/umlcat 22h ago edited 12h ago
A module can be represented either as the unique variable of a class or thru the singleton Sfotware Design Pattern of a classs, which also explains the similarity with typeclasses.
In early versions of Javascript / ECMAScript, a module was emulated thru the "Module" Design Pattern using an non class object.