Type class is a useful concept to let a function process different types of data in a controlled approach. Scala provides implicity parameter, context bound and the
implicitly function to support the use of type class. This is a summary of the purpose and usage of type class. The later parts is primarily based on the the Youtube video Don’t fear the Implicits: Everything You need to Know About Typeclasses and Polymorphism in Scala
There are three types of polymorphism:
- subtyping polymorphism: the super-type sub-type relationships in object oriented context. It is not recommended for Scala because the
is-arelationship is too restrict and hard to maintain.
- parametric ploymorphism: generic class and generic trait are type constructors that create parameterized types.
- ad-hoc polymorphism: type-specific methods that invoke different implementation for different types. Method overloading/overriding are ad-hoc polymorphism. Scala also provides implicit parameters and other construct to suport ad-hoc polymorphism, similar to the
type classin Haskell.
The Ad-hoc polymorphism and type classes blog gives a clear defintion of
Type class is a concept of having a type-dependent interface and implicit implementations of that interface, with separate implementation for each supported type.
In Scala, methods in a generic trait define the interface of a type class. Define objects of the parameterized class as an implicit parameter for different types.
The essence of type class is to let a method use a parameter whose type is from a set of types that don’t have a common super-type, while control the set of types used as the parameter types. This Type-class implicits blog explians the motivation. Scala has constructs (implicit parameter, context bound and
imlicitly method) to make it easy. The context bound
[A: B] specifies the parameter type A should come with an implicit value of type
B[A] in scope.
The implementation of a case class invloves the following steps.
First, the operations are defined in a generic trait, the type class.
Second, in the companion object of the type class (or other objects), create an instance for each type
Third, in any place, usually in the companion object of the type class, define the functions that use the data and its operations.
The context bound
[A: B] will add an implicit paramter to the method that can be accessed using the
Therefore, it is easy to make it to work with any type, as long as an implicit instance of
TypeClassOps[A] is in scope, the
userOps works. There is no need to change that type.
The optional fourth step is to define an
implicit class for each class that supports the typeclass methods using the constructs. This enriches a class with the additional trait methods. This is an addon, not an essential goal of typeclass.
- Using implicit parameters we can use a common interface for a T if T implements the interface or the implementation is in its implicit scope.
- Using implicit instance we can have multiple implementations for a single type and bring a specific instance into scope.
3 Implicit Resolution
The order or implicit resolution, from high to low, is:
- Package object
- Implicit scopes
- companion object of type class
- companion object of A
- companion objects or super types
The compiler reports an error if no implicit value found or more than ones in the same order level.
The following patterns easy the use of type class
The use of
def op1(a: A) can be simplified by defined an implicit class:
The above implicit class allows
a.op1() call from an instance of
A, adding a new method
op1 to the class.
4.2 Error Message
To give better error message, add a notation to the type class. An example is
4.3 Shortcut for
If we just want to call the methods defined on the typeclass, in type class’s companion object, define an
apply[A](implicit a: TypeClassOps[A]) = a to short the
TypeClassOps[A] when context bound is used. The call will be
4.4 Type Class Contructor
It is a common practice to define factory methods to create the implicit instances of the type class. The factory implementation is type-specific but usually can simplifies the creation of the implict instances. For example, you can create an implicit
Ordering[A] instance using
Type classes often provide combination or transformation operators. The purpose is to reuse existing instances or derive from those instances.
4.5.1 Simple Field
Following is an exmple:
4.5.2 Generic Type Class Instances
Also called implicit derivation. In this case, an existing implicit instance is used to generate an implicit instance of a generic class such as
4.5.3 Derving Instances
It is possible to derive instances of
Foo[A] from instances of
Bar[A]. For example, Akka HTTP allows plugging in type class based JSON libraries. It defines a type class
ToEntityMarshaller that converts any Scala types to Http request/reponse bodies. The
ToResponseMarshaller that coverts to Http response.
The above code creates an
ToEntityMarshaller[String](the type of
4.5.5 Default Instances
Provide default instances if they make sense. To make it easy to opt out default instances, put default instances in companion objects that can be easily replaced by local or inherited instances.
Don’t provide default instances if they don’t make sense. For example,
AnyRef might not be a good idea because it is too general.
Try to make case classes serializable because they are often sent over network.
5 Questions About Type Class
Try to answer the following questions:
- Is retroactive extension important? Case class allows you to work with existing unchangable types.
- Can you provide good defaults? Only define defaults when they make sense.
- What is the tradeoff of boilerplate/clarity? Automatically deriving instances could be confusing, especially when several type cases involved.
- Can you support two usage models, one not relying on implicits?
scodecis a good example that provides implicit and non-implicit usages.
The typeclass hierarchy in Cats lib: All things you traverse.
6 Typeclass Laws
- The pirmay function of type class is to enable retroactive extension.
- It is an alternative to subtyping and adpater approach.
- Typeclass works on types, not values.
- It is better to provide simple constructors and combinators.
- Automatic derivation is powerful.
- Interactions of multiple typeclasses are hard to understand.
- Typeclass can have laws to allow concurrent computation.