Scala 3: Contextual Abstractions, Part II

Dean Wampler
Oct 28, 2020 · 3 min read

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.

Reflections, The Sullivan Center, Chicago ©2020, Dean Wampler, All Rights Reserved

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:

Semigroup and Monoid definitions

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 combine and <+>. We will only need to implement combine. However, 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 Int and String:

Instantiations for Int and String

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:

Using our Monoid instances

StringMonoid and IntMonoid objects were created, which hold the implementations of unit. The <+> operator is an extension method that we can use on instances of Strings and 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[Monoid[String]], where 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 Numeric[T]:

Given “instance” (actually a class) for all Numeric[T]

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 T, a 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 Numeric[T] argument.

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 obj1.method(obj2).

What’s Next?

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.

Scala 3

What’s new in Scala version 3

Scala 3

A series of posts on Scala version 3, what’s new and why, and how to use its new features effectively. For more details, visit http://programming-scala.org/.

Dean Wampler

Written by

The person who’s wrong on the Internet. ML/AI and functional programming enthusiast at Domino Data Lab. Speaker, author, aspiring photographer.

Scala 3

A series of posts on Scala version 3, what’s new and why, and how to use its new features effectively. For more details, visit http://programming-scala.org/.