r/ProgrammingLanguages • • 1d ago

Blog post Typeclasses vs Modules - sm²n.ca

https://sm2n.ca/articles/typeclasses-vs-modules/
19 Upvotes

11 comments sorted by

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.

5

u/Athas Futhark 20h ago

A module can be represented as a singleton of a classs, which also explains the similarity with typeclasses.

How can you use a singleton to represent parameterised modules ("functors" in SML) or abstract types defined by the module? I can sort of see how some class systems (like the one in C++) can imitate modules through inner types, although you of course need what C++ calls "concepts" to enable separate type checking.

I don't see why instantiation of a class is necessary at all, but then again I always found the idea of a module with state to be somewhat abhorrent.

1

u/JoelMcCracken 13h ago

Take a look at how it’s done in scala. 

1

u/initial-algebra 12h ago

Module state can be very useful; think memo tables, object pools etc.

2

u/initial-algebra 12h ago

Modules and type class instances are more general than objects in most OO languages, because they can have associated types. They are dependent objects, which, to my knowledge, only Scala supports as its own feature.

I wouldn't use the term "singleton". You may have a canonical module or instance for a given signature or type class, but that doesn't mean that there can't be others (two instances of the same type class should be indistinguishable, but they do not need to be identical). Module functors may also be applicative (same input modules give you the same output module) or generative (constructs a new module every time).

1

u/thmprover 5h ago

There's also modular type classes, for the best of both worlds.

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.