Journey of Implicits in Scala — part 3

ayush mittal
7 min readMar 16, 2022

Type Classes in Scala

This is the third part of a 5 part series to understand the concept of implicits in Scala . The road taken is as follows

  1. Introduction to implicit methods or variable definitions and method parameter lists.
  2. Type Annotations in Scala
  3. Type classes in Scala
  4. Implicit hell and how painfull it really is?
  5. Scala 3’s approach to Type Classes.

In this part we will see how to encode type classes and how type classes can lead to ad-hoc polymorphism.

Let’s start directly with an example. We look at how we can convert a case class to an HTTP Response.

case class User(id: Long, name: String)object ResponseUtils {
def createdResponse(user: User): HTTPResponse =
HTTPResponse(201,s"http://localhost:8080/${user.id}")
}
ResponseUtils.createdResponse(User(1l,"John Doe"))

We define a case class User . We would like to create a HTTP 201 created response from this. We use the ResponseUtils.createdResponse method , which uses the id from User to specify the user resource location.

Soon we realise that we need a similar method for other entities in our domain. So, we add methods similar to the one already present for User

case class Employee(id: Long, name: String)object ResponseUtils {
def createdResponse(user: User): HTTPResponse =
HTTPResponse(201,s"http://localhost:8080/${user.id}")

def createdResponse(employee: Employee): HTTPResponse =
HTTPResponse(201,s"http://localhost:8080/${employee.id}")
}
ResponseUtils.createdResponse(Employee(1l,"Tom Harry"))

For another case class in our domain Employee , we added a new overloaded method createdResponse to create an HTTP response for Employee. This is a class polymorphism with method overloading where we write two or more methods in the same class by using same method name, but the passing parameters is different.

We have a working solution but something is still amiss. Both the overloaded method’s have almost the same definitions. Both these classes have something in common which is all we need in the createdResponse method. Both of them have an id. Let’s use that common feature of both classes to create a super class/trait and then use the trait in our createdResponse method.

trait Id {
def id: Long
}
case class User(id: Long, name: String) extends Idcase class Employee(id: Long, name: String) extends Idobject ResponseUtils {
def createdResponse(id: Id): HTTPResponse =
HTTPResponse(201,s"http://localhost:8080/${id.id}")
}
ResponseUtils.createdResponse(User(1l,"John Doe"))
ResponseUtils.createdResponse(Employee(1l,"Tom Harry"))

Each of our entity class — User, Employee now extend an Id trait. The ResponseUtils.createdResponse can now take a type of Id to create the HttpResponse. Above code can also be written as

trait Id {
def id: Long
def createdResponse(id: Id): HTTPResponse =
HTTPResponse(201,s"http://localhost:8080/$id")
}
case class User(id: Long, name: String) extends Idcase class Employee(id: Long, name: String) extends IdUser(1l,"John Doe").createdResponse
Employee(1l,"Tom Harry").createdResponse

With this approach we have moved the desired method inside the parent class and we are directly calling the createdResponse method on the User and Employee instance. This has a nice feeling to it. We can also override the createdResponse method in the sub-classes if required. This is another classic example of polymorphism using method overriding.

However with this approach we loose a lot of flexibility. Our entity classes have to extend a trait. What if we come across an entity that cannot be changed. If its a class that is outside our control and the source code cannot be changed?

Let’s look at the problem from another angle. We need to use the id property of the the entity classes. If not use them directly , we need to provide some proof that an id will be available. Let’s encode this proof using a type class pattern. If you remember the discussion in series 2: Type is a set of things which share same characteristics. Members of a type belong to that set. class or interfaces or traits are ways to create members of that set. The characteristic here is that they all have an id.

trait HasId[T] {
def getId(t: T): Long
}

If we manage to get your hands on an instance of HasId[T] for a given T, you know how to retrieve its id. We can use User , Employee as type parameters to create new types.

object UserObject extends HasId[User] {
override def getId(user: User): Long = user.id
}
object EmployeeObject extends HasId[Employee] {
override def getId(emp:Employee): Long = emp.id
}
object ResponseUtils {
def response[T](t: T, idProof: HasId[T]): HTTPResponse =
HTTPResponse(201,s"http://localhost:8080/${idProof.getId(t)}")
}

And now we can use it like this

ResponseUtils.response(User(1l,"John"), UserObject)
ResponseUtils.response(Employee(1l,"John"), EmployeeObject)

When we look at the definition of ResponseUtils.response, it is exactly as polymorphic as we want. It exposes an operation which takes any entity T, as long as there is an implementation of HasId for that entity.

The HasId[T], which up until now has been referred as the “proof” is the Type class.

It is the trait that abstracts the constraint that is used to deliminate which types would be allowed to be operated on polymorphically.

The UserObject and EmployeeObject above are referred to as the Type class instance. User and Employee above are the objects that need to be operated on in a uniform way. And they are linked to the type class instance by being the type parameter in the definitions of the type class instances.

And last but not the least, we have the ResponseUtils.response definition itself that provides an interface for us to use the code in a polymorphic fashion which is

object ResponseUtils {
def response[T](t: T, idProof: HasId[T]): HTTPResponse =
HTTPResponse(201,s"http://localhost:8080/${idProof.getId(t)}")
}

So, in summary, the design for encoding the type class pattern consist of the following components:

  1. A Type class (a trait with type parameters that describes the evidence)
  2. Type class instances (implementations of the trait for each object required to provide an evidence)
  3. The interface via which the polymorphic operation is applied

Above three are the essential part of the type class pattern. There is one more crucial thing that makes this design of the machinery complete in Scala.

That crucial thing is the implicit feature to make supplying the type class instance a lot more seamless. Basically, we will be making the Scala compiler work for us.

Making use of Implicits

Looking at the code snippet below:

object ResponseUtils {
def response[T](t: T, idProof: HasId[T]): HTTPResponse =
HTTPResponse(201,s"http://localhost:8080/${idProof.getId(t)}")
}

In the ResponseUtils.response method, one needs to explicitly pass in the t (Entity) together with its type class instance (HasId [T]).

But would it not be cool to be able to only specify the entity, and let the HasId be automatically provided for us by the Scala compiler? And if the Scala compiler cannot find the required proof of id (i.e. the required type class instance), then the compilation should fail?

This is possible by making use of implicits. To do this, we slightly modify the definition of ResponseUtils.response. This looks like this:

object ResponseUtils {
def response[T](t: T)(implicit idProof: HasId[T]): HTTPResponse =
HTTPResponse(201,s"http://localhost:8080/${idProof.getId(t)}")
}

We break the parameter definitions into two, and we tag the last one with the implicit keyword, which would make it a candidate to be automatically provided for us by the Scala compiler in cases where it is omitted.

The next thing we need to do is to mark our concrete evidence, our type class instances with the implicit keyword. This would make them viable options to be included in the parameter list by the Scala compiler.

implicit object UserObject extends HasId[User] {
override def getId(user: User): Long = user.id
}
implicit object EmployeeObject extends HasId[Employee] {
override def getId(emp:Employee): Long = emp.id
}

Let’s take a look at these changes together with the rest of the code snippet

case class User(id: Long, name: String)
case class Employee(id: Long, name: String)
trait HasId[T] {
def getId(t: T): Long
}
implicit object UserObject extends HasId[User] {
override def getId(user: User): Long = user.id
}
implicit object EmployeeObject extends HasId[Employee] {
override def getId(emp:Employee): Long = emp.id
}
object ResponseUtils {
def response[T](t: T)(implicit idProof: HasId[T]): HTTPResponse =
HTTPResponse(201,s"http://localhost:8080/${idProof.getId(t)}")
}
ResponseUtils.response(User(1l,"John"))
ResponseUtils.response(Employee(1l,"John"))

In the above example, the missing parameter list is automatically provided because they are in the local scope. As usual, the Scala compiler would scan the implicit scope for values that are suitable.

In cases where we pass in an Entity that does not possess an id proof, aka, a entity without an instance of HasId[T] in implicit scope, then the compilation would fail. For example something like this:

ResponseUtils.createdResponse(SomeOtherEntity(1l,"John Doe")) 
// fails at compile time

The last thing we are going to touch on is how Context bound and implicitly function, two other language features of Scala that are usually applied when putting type class pattern together.

Using Context Bound Annotation and the implicitly function

If you take a look at the definition of the response method, and find a way to make it less verbose. That is:

...
def response[T](t: T)(implicit idProof: HasId[T]): HTTPResponse
...

A context bound annotation like [T: Bound] which allows us to specify that for any type T there must be an implicit value of Bound[T].

Which in essence, is what we are doing. We want that for any entity T there must be an instance of HasId[T].

You can go check out the 2nd post of this series for an overview of context bound annotation.

Modifying the code, to include usage of context bound would leave us at:

object ResponseUtils {
def response[T: HasId](entity: T): HTTPResponse = ???
}

As we can see, since we have removed the (implicit idProof: HasId[T]) portion of the method definition, we would no longer have access to getId in the method body.

But this is where the implicitly function comes to our rescue. The implicitly function allows us to reach into the implicit scope and get hold of an implicit value of interest.

In this case, we need to grab a hold of an implicit value of type HasId[T], which was what the idProof variable was. but since it is now gone, we can replace it with: implicitly[HasId[T]].getId(t)

Taking together the updated code snippet would look like:

case class User(id: Long, name: String)
case class Employee(id: Long, name: String)
trait HasId[T] {
def getId(t: T): Long
}
implicit object UserObject extends HasId[User] {
override def getId(user: User): Long = user.id
}
implicit object EmployeeObject extends HasId[Employee] {
override def getId(emp:Employee): Long = emp.id
}
object ResponseUtils {
def response[T: HasId](entity: T): HTTPResponse = {
val id = implicitly[HasId[T]].getId(t)
HTTPResponse(201,s"http://localhost:8080/$id")
}
}
ResponseUtils.response(User(1l,"John"))
ResponseUtils.response(Employee(1l,"John"))

And with that, we are able to make use of context bound annotation ( together with the help of the implicitly function) to make the type class encoding a little less verbose.

In the next post, we will see some issues that might be faced by developers when using libraries that are based on this type class pattern.

Further readings

--

--