My last post discussed implicit conversions and extension methods. This post describes the Scala 3 way of defining type classes.
Update: November 17, 2020: Refined the remarks about using anonymous type classes instances instead of the named instances.
Update: December 27, 2020. The syntax for given instances and extension methods was refined slightly in Scala 3.0.0-M3.
We can now add extension methods to types, but what if we want to implement an interface, so that every type for which we define such methods are implemented with the same signature? Scala 3 type classes meet that need.
But first, what are type classes? This term comes from Haskell where this concept originated. The word class here doesn’t have exactly the same meaning as in most OO languages, except in the general sense of things that are the “same” in some way can be grouped together.
Type classes define an abstraction that can be implemented for many types, then define the specific concrete implementations for specific types. Here is an example in Scala 3 syntax:
In Mathematics, Semigroup is the abstraction of addition, as suggested by the made-up operator name
<+>. Monoid is Semigroup with a
unit value, like
0 in numeric addition.
Note that a trait is used to define the abstraction. For the supported types
T, extension methods are used to add
<+>. We will only need to implement
unit is not defined as an extension method. This is because one
unit value works for all instances. We’ll see how all this gets implemented in a moment.
Here are the type class instances for
The implementations are straightforward. Recall from the last post that
given foo: Bar is the new alternative to declaring an implicit value. If you enter these definitions in the Scala 3 REPL, you’ll see that
objects are being defined.
Now let’s try these instances:
IntMonoid objects were created, which hold the implementations of
<+> operator is an extension method that we can use on instances of
Ints. The Semigroup operation must be associative, as shown.
We could have made the instantiations anonymous,
given Monoid[String], for example. In this case, to reference
unit, you would normally use
summon works exactly the same as the old
implicitly method for “grabbing” a reference to an implicit value in scope. Or you could use the default name,
given_Monoid_String, but this would hard code a compiler convention that could change in a future release of Scala.
This example nicely encapsulates how to use type classes to implement a common abstraction in many target types, including the equivalent of companion object members,
unit in this case. However, the actual object isn’t the companion object of the type, if there is one.
Finally, it’s possible to instantiate a type class with parametric behavior. In the example, we shouldn’t have to define an instantiation for every different numeric type. Here’s how to generalize the
IntMonoid to any
Note that new using clause,
using num: Numeric[T], which replaces the corresponding implicit parameter list in Scala 2. I’ll discuss it more in the next post.
To be sure you understand the first line,
NumericMonoid is the name we’re giving this instance and
Monoid[T] is the type. Because it has a type parameter
class is actually created, not an
object like the examples above. When we write
NumericMonoid[BigDecimal], for example, an instance of that class is instantiated for
BigDecimal. The using clause is the constructor argument list for the class, but you will probably never provide an explicit
Also note how
unit is referenced. In line 9, we have to specify the type parameter, while in line 8, it can be inferred because of the
BigDecimal instance on the left-hand side of
<+>. Scala’s type inference isn’t “symmetric” across
In the next post, I’ll explore the new using clauses, which replace implicit parameter lists.
You can start reading the rough draft of Programming Scala, Third Edition on the O’Reilly Learning Platform. Currently, the first six chapters are available, including the two (five and six) that cover contextual abstractions.