In the first part of this article I presented some alternatives to the type classes pattern while emphasizing the disadvantages coming with them.
Type classes in Scala
Is there a better way to solve the problems presented in the first part and
have ‘the best of the two worlds’, meaning, to code against the Document
interface while still being able to use the Aggregate[Document]
view and, at
the same time, keep the Document
interface decoupled and free of orthogonal
concerns?
It seems like, functional programming languages offer an alternative, superior in terms of modularity and extensibility. This solution is called the ‘type class pattern’.
What follows is the way to implement the pattern in Scala.
The definition of the type class itself:
An aggregate view provider is marked as being implicit
.
Another implementation artifact is a parameterized method that will take care of the conversion.
A couple of remarks:
-
Using the parameter
implicit c: AggregateAdapter[A]
or the more abbreviated formimplicitly[AggregateAdapter[T]]
results in the Scala compiler supplying an instance of the requiredAggregateAdapter
from the lexical scope. Therefore, you could easily define, in different scopes, two or more different implementations of the same type class for the same type. -
By using the
asAggregate()
method, the client code will not have to deal with adapters, but instead will use the type it’s interested in directly.
How about Haskell?
Why Haskell? Type classes were originally developed in Haskell as an alternative to ad-hoc polymorphism.
Like in the case of Scala, this is not a Haskell tutorial, therefore I won’t insist on the Haskell features.
Since adopting the previous use case for Haskell requires language features that would obscure the example, I will use a more convenient scenario.
Assume that a hypothetical feature discovered during the design phase is the
necessity for some entities in the domain model to be Printable
(possibly
because of the need to send a representation of them to a printer). What follows
is the same reasoning as in the case of Aggregate
s.
A couple of remarks resulting from examining this code:
-
Haskell has built-in support for type classes. The keyword
class
is used to define a type class. The keywordinstance
is for providing the definition of what, in a certain scope, means to apply the type class to a certain type. Another notable observation is the fact that the definitioninstance Printable FileSystem where
defines an instance of the type class, but than instance is not named. -
Haskell is much more terse than Scala. However, Scala shines in a different way, and I will explain what this means in the next section.
Conclusion
There are a few concepts worthy of jolting down :
-
It is said that a type is added to a type class (and becomes a member of it) if the required definitions can be provided.
-
Members of a type class are at any time dependent on the current scope. Therefore you don’t care if the creator of a type anticipated the type class you want it to belong to. If required, you can simply create your own definition and then use it accordingly.
-
A type does not have to know it is a member of a type class (does not
extend
that type class), and it can be added to new type classes without the need to modify it. Unlike using normal interfaces, where you would have to make that type implement the interface.
In Scala’s case the search for type classes is performed locally in the scope of the method call that triggered it. And because it’s an explicitly named instance in Scala, you can inject another instance into the scope and it will be picked up for use for the implicit. In contrast with Haskell, whose instances are anonymous, therefore the Haskell compiler will look into a global name space to resolve the instance in the current scope, which is very limiting.
To sum up, a review of what type classes are and how they serve you:
- Type classes will enforce a contractual relationship between an implementer and an implementee (same as interfaces).
- The type classes can serve as a bridge pattern – gaining separation of orthogonal concerns (same as adapters).
- An interesting application is called
retroactive extension
. This means you can ‘attach’ new behavior, concerns or, in other words, retrofit existing types whose source code is inaccessible.