Retroactive Polymorphism with Scala Typeclasses :: Jan 31, 2017
As an object-functional language, Scala supports many ways of writing generic, polymorphic code. This article will introduce retroactive polymorphism, and examine the advanced language features enabling it in Scala.
Suppose we are writing a function sum
that combines the elements of a sequence. The definition is simple with a numeric type like scala.Int
or java.lang.Integer
.
Scala
Java
When sum
is generic, however, the elements’ capability to combine must be explicitly stated. One approach is declaring an interface Semigroup[A]
,
Or in Java:
Types implementing Semigroup
can now be passed to sum
But it’s impossible to retroactivly declare types we don’t control (Like scala.Int
) as Semigroup
s. Scala offers two workarounds: the Adapter Pattern and Type Classes.
Lets first consider Adapters. Popularized by the Gang of Four and used in Java, we define generic wrappers for every type needed:
But this requires users to laboriously create wrapper objects, which is both wordy and unperformant:
Typeclasses circumvent the Adapter Pattern’s issues, at the potential cost of greater complexity. Instead of extending type T
with an interface to declare some behavior B
, a typeclass implementation is an external object able to perform B
on T
. Beginning with the simplest of typeclasses, this post and its follow-ups should progress to the advanced definitions seen in the wild.
As implementations of the typeclass Semigroup[A]
will be themselves combining elements of type A
, a second parameter b
is added to Semigroup.combine
:
sum
can now be redefined as:
And used after implementing Semigroup
for Point
and Int
There are still many issues with our typeclass, namely:
Semigroup
instances must be manually passed around- It would be preferable in many cases to use infix notation
1.combine(2)
vsintSemigroup.combine(1,2)
Implicit parameters solve the former. If a second implicit
parameter list is added to sum
, the compiler will attempt to find an implicitly declared instance, first using static scoping rules, and finally by checking the typeclass’s static fields. Redeclaring sum
and intSemigroup
,
Or alternatively using Context Bound syntax,
sum
can now be called without passing intSemigroup
Implicits also permit a form of ad-hoc polymorphism when used with typeclasses; should a library designer declare common instances in the typeclass’s companion object, users are free to pass or implicitly declare alternate definitions.
To scrap some boilerplate, we define static Semigroup.apply[A]
to return the implicitly required Semigroup[A]
.
Although I find the context-bound Semigroup.apply
more appealing, the first version should be used because it can be inlined if desired.
Next up is infix syntax. Scala has implicit classes which allow just that:
Regular implicit classes create a lot of wrapper objects, so SemigroupOps
should be restructured as a Value Class, which doesn’t allocate any runtime objects when used correctly.
Conclusions
Typeclasses are a powerful means of achieving retroactive polymorphism. In future posts, I hope to cover the different styles used by open source libraries like spire and cats, with usability and performance considerations.
The final code can be found here
Notes
It should also be mentioned that while Structural Types can be used in similar ways, the feature relies on runtime reflection and is discouraged from use.