extension (inline x: Byte | Short | Int | Long)
transparent inline def rotl(j: Int): Int | Long = inline x match
case i: Int => java.lang.Integer.rotateLeft(i, j)
case l: Long => java.lang.Long.rotateLeft(l, j)
case _ => compiletime.error("rotl (rotate left) is defined only on Int and Long\nother primitive types are not promoted")
You need inline at all the places stated: on the argument on the way in, on the match, and on the method itself.
It may be my lack of familiarity with typeclasses, but my reaction is that I can’t see why anyone would prefer this approach. It look’s like you just did overloads with extra steps. There’s a large boilerplate overhead with lots of confusing keyword tricks going on… – maybe scala have really benefited from a dedicated typeclass keyword instead of having you implement the idea through companion objects and implicits (no one who is not already familiar with typeclasses is going to get what this code is doing, but at least if they saw the word “typeclass” they could google it)
Is there another example I could see that typeclasses solve which are not possible as nicely by other techniques?
I once wrote a little bit about the topic it may be helpful: Polymorphism in Scala. · GitHub
But the TL;DR; is that there are two major advantages to typeclasses.
They allow “extending” classes you don’t control.
They are resolved at compile time based on types, thus they usually provide the kind of type relationships that you are trying to achieve here using pattern matching.
There’s a large boilerplate overhead with lots of confusing keyword tricks going on… – maybe scala have really benefited from a dedicated typeclass keyword
It’s not keyword tricks, it’s really pretty central to the language. And there is dedicated syntax for it in Scala 3, which was one of the main changes between 2 and 3 – that’s the given and using keywords. (I’m just not as practiced in them myself yet, so I used the Scala 2 implicit keyword.)
@BalmungSan gives a couple of reasons why type classes are a powerful tool. More specifically, the problem of “I want to return the same type that was passed in” turns out to be challenging to get consistently right in traditional OO programming (especially when subclasses get involved), but is relatively easy to handle correctly in type classes.
(See the “F-bounded polymorphism” article that @spamegg1 linked above, which is the usual explanation of this problem – it doesn’t come up super-often, but it can be a doozy of a disaster when it does – I’ve lost days trying to get the F-bounded approach working when doing OO-style code, and you usually don’t realize that you’re in trouble until you’ve already written a ton of code.)
Also, type classes are, essentially, interfaces. Note that my Oneable above is a type unto itself, which can be passed into higher-level functions; overloads can’t easily do that. That’s super useful, and comes up in real-world code all the time. It means that my higher-level code doesn’t even need to know or care what the concrete type is, just that it is something that is Oneable.
I should be clear: for your toy example, overloads are fine. But real-world code is usually far more complex – you usually find that you gradually have several different types for which you want to implement a behavior (not just two), often scattered around your codebase, so overloads wind up becoming a centralized and ugly maintanance headache, whereas type class implementations can be distributed around the code as desired.
No, it isn’t the absolute most-concise code one could wish for. But it often turns out to be more correct, more flexible, and more extensible than overload-based solutions, and that’s more important 99% of the time.
It’s by no means the only way to do things (Scala has many tools available), but when the problem is essentially “I have a behavior that I want to implement for multiple unrelated types”, it’s usually the best approach for producing solid, maintainable code.
The result is that I use overloads very rarely – they’re cute, but not very powerful and sometimes more confusing than helpful. (Indeed, I know folks who just plain outlaw them in their codebases, on the grounds that they are more trouble than they are worth.) If I want an overload, what I really mean is that I’m trying to implement the same behavior for multiple types, and type classes usually pay off in the long run as a better way to do that.
jducoeur:
The result is that I use overloads very rarely – they’re cute, but not very powerful and sometimes more confusing than helpful.
I don’t find that to be particularly true. I use them moderately heavily; the key is to understand when they’re the best tool for the job.
Typeclasses show their power most when you want to abstract over data, especially if you want to be able to pick anything you can get your hands on and give it the capability.
But overloads show their power most when you want to encapsulate functionality in a class that might be doing something nontrivial but you want to present an API that fluently* exposes what you can do.
(* In the colloquial sense, not the programming style sense.)
For instance, suppose you have a class whose job it is to fetch some data and compress it using a novel streaming algorithm. Having a high-level compress method that takes File, Path, URI and maybe InputStream is a splendid way to expose the functionality. There is no sensible reason to have a CanCompress[A] trait–the class’s job is to make it so you don’t need to know all the different tricks employed to get the data in the right shape with the different input methods.
On the other hand, Ordered is practically the canonical example of something you want done by typeclass, and possibly a set of different typeclasses, because if you’re sorting, all you need is a sane definition of <, and you’re good. The best way to reproducibly pass around that information, and let everyone sort using your sorting algorithm, is to define a typeclass.
So: use the right tool for the job. Typeclasses are more powerful, but not always well-suited to the task.
jducoeur:
It’s not keyword tricks, it’s really pretty central to the language. And there is dedicated syntax for it in Scala 3,
I meant analogous to how in languages that had not yet implemented dedicated class constructs, people discovered clever ways to achieve the “idea” of what classes are through combining other features of the language, like JS creating constructor functions with the “prototype” blah blah balh, before JS finally added a first-class way to create classes by simply saying class. Or C++ lacking abstract methods, so they have a convention of making virtual functions that at first are set to 0 (I think? I’m not c++ programmer))
This seems like one of those moments. In Scala, this doesn’t look like “Typeclasses: the feature”, it looks like “Typeclasses: the design pattern”.
I would expect you to just have a typeclass keyword, but instead I have to create a trait, then companion object by the same name, then scatter using/implicits everywhere… To “achieve the idea” of type classes indirectly. Is it like this in all languages, or are there FP languages that make the notion of type classes feel more built-in to the langauge? Like… the word “typeclass” does not even show up in your code. How would I know that’s what you’re doing? Unless I also had personally memorized the ceremony and tradition.
Scala has a slightly more general and powerful feature than typeclasses that exist in other languages. The key question about typeclasses from a conceptual perspective is whether they are unique (sometimes called “coherence”).
The easy-mode answer to the question (for both you and language correctness) is that either a type does have a trait (typeclass) implementation or it does not. It’s a little bit less easy because you have to figure out some way to enforce that there can be only one, but given that there can be only one, it’s pretty simple to think about.
However, this also fails the Ordered test! For instance, Rust has coherent trait implementations, which means that all its sorting routines have to resort to ridiculous antics like wrapping everything in newtypes just because you want to sort them in the opposite order.
Scala’s allowance for there being multiple possible implementations depending on context, with a sophisticated method for propagating and computing context, is much more powerful.
But it’s not, at that point, just typeclasses. You can do typeclasses with Scala traits and context. Occasionally there is talk about supporting them specifically, and the pushback is always: but does that really help anything?
There is a little bit of support. You can write def sort[A: Ordered](xs: Seq[A]) where Ordered is not the type of A but rather a declaration that a context parameter of type Ordered[A] is expected to be found. It acts very much like a type, so it looks very much like a type.
But there isn’t really any need for a declaration that something is a typeclass because, really, it’s just a trait that has a generic type parameter. So, yes, it’s kind of a design pattern in that sense. But that doesn’t mean that it’s substandard in any major way–just that it doesn’t make much sense to have separate syntax for a strictly weaker feature if you already have the more powerful feature.
Is it like this in all languages, or are there FP languages that make the notion of type classes feel more built-in to the langauge? Like… the word “typeclass” does not even show up in your code. How would I know that’s what you’re doing? Unless I also had personally memorized the ceremony and tradition.
Yes. It’s culture / tradition. (Just like almost all things humans do)
“Type class” is not a strictly, formally defined concept, it’s loose, think of “type class” as the “folklore name”.
The somewhat more proper name is “ad-hoc polymorphism”. This is also not strictly formally defined, and type classes are only one way of doing it (there are other ways).
It also does not help that multiple ideas get all tangled up together often: “interfaces”, “implicits” and “typeclasses”. You have to know each idea separately and clearly to understand how they are fused, so you don’t get confused. (I’ll let myself out
)
More generally, almost all things in programming languages are loosely, culturally defined; and different languages implement them differently, with different keywords. Very similar to natural languages if you think about it (dialects, same word meaning different things, etc.)
More or less the same in other FP languages yes. None of them have the “typeclass” keyword.
Haskell calls it “class” A Gentle Introduction to Haskell: Classes Haskell also has the “type” keyword for type aliases (like Scala) and “datatype” keyword for defining actual data types.
Lean also calls it “class” following Haskell very closely Overloading and Type Classes - Functional Programming in Lean
Standard ML has no such keyword, but they implement the idea of an “interface” via the “signature” keyword, an example: The STRING signature They don’t have “typeclass” or “class” but “structure” instead
They also have the concept of “functor” (which is often associated with typeclasses) but no keyword for it SML Basis Library Overview
And it goes on.
I would expect you to just have a typeclass keyword, but instead I have to…
Expectations are a bad idea when it comes to programming languages… I advise against it
Unfortunately it’s a mess just like natural languages, you have to learn the stupid idiosyncrasies.
I went through the FP tradition like this: Racket → Standard ML → Haskell → Scala → Lean