Implementing Type classes in Scala 3

Scala 3 aka Dotty has been a topic of discussion in Scala community for almost 4 years now. The announcement of Scala 3.0.0-M3 release is a big milestone in this journey and paves way for a RC1, which is tentatively planned for January 2021.

There are lots of new features and improvements in Scala 3. Martin Odersky’s talk is a must for anyone looking for a sneak peek into the language’s future. New constructs like Intersection and Union types focus on functional programming foundations while simplifications like Trait parameters and Opaque types are meant to make Scala more productive for day programmers.

We shall look at how to implement Type classes in Scala 3. Type classes are a powerful tool in functional programming to enable ad-hoc polymorphism without using OOPS based constructs like sub typing. For an intro to type classes check this page.

First step in a type class implementation is trait. This trait introduces an idea. Ours is that of transforming a type A into an `httpresponse`.

trait HttpResponse[A] {
def response(a: A): Response
}

The second step is to provide some default implementations of your type class trait in its companion object. This is how we would generally achieve it in Scala 2.x versions.

object HttpResponse {implicit object StringHttpResponse extends HttpResponse[String] {
def response(str: String) =
Response.status(OK).entity(str).build()
}
implicit def OptionHttpResponse[T](implicit httpResponse:
HttpResponse[T]) = new HttpResponse[Option[T]] {
def response(opt: Option[T]) = opt match {
case None => Response.status(404).build()
case Some(t) => httpResponseLike.response(t)
}
}

We can use the default implementations to create implementation for other containers of that type. Here we are using the http response like behaviour of T to create a http response like behaviour for Option[T].

The usage would be generally by creating a trait that exposes the transformation or by creating an interface syntax.

implicit class ResponseOps[A](a: A) {
def response(implicit h: HttpResponse[A]): Response =
h.response(a)
}
"all is good".response
//Response{status=200, reason=OK}
val someValue: Option[String] = Some("all is good")
someValue.response
//Response{status=200, reason=OK}

All this was a recap. Now let’s try to achieve the same functionality in Dotty with features like extensions methods, givens and using clauses. I am going to use dotty’s latest indentation syntax because you know..its cool !!

Here is our trait that defines the idea of creating a http response from type A. We introduce a new keyword called extension .

trait HttpResponse[A]:
extension (a: A) def response: Response

Extension methods allow us to add methods to a type after the type is defined. We shall take another example to understand this concept.

Suppose we have a class representing emails in our domain

final case class Email(value: String)

We would like to add a method to the Email class without changing the class itself. The way to do that using Extension methods would be to define an extension on the type.

extension (e: Email) 
def username: String = e.value.takeWhile(_ != ‘@’)
val email = Email(“john.doe@gmail.com”)email.username // john.doe

Hopefully the above example has cleared the concept of extension methods.

Now coming back to our original type class. We need to provide concrete implementation of HttpResponse type class. We shall do that for the type String. We can provide them using Given syntax

given HttpResponse[String] with
extension (str: String)
def response: Response = Response.status(OK).entity(str).build

Above examples defines an anonymous given. We can also bind it to a name.

given stringResponse : HttpResponse[String] with
extension (str: String)
def response: Response = Response.status(OK).entity(str).build

Given instances (or, simply, givens) define canonical values of certain types that serve for synthesising arguments to context parameters. Let’s understand that by providing implementation of HttpResponse type class for the type Option .

given optR[T](using t:HttpResponse[T]): HttpResponse[Option[T]] with
extension (opt: Option[T])
def response: Response =
opt match
case None => Response.status(404).build()
case Some(t) => t.response(t)

optR[T] defines givens for HttpResponse[Option[T]] for all types T that come with a given instance for HttpResponse[T] themselves. The using clause in optR defines a condition: There must be a given of type HttpResponse[T] for a given of type HttpResponse[Option[T]] to exist. Such conditions are expanded by the compiler to context parameters. The condition can also be specified using bounds.

given optR[T : HttpResponse]: HttpResponse[Option[T]] with
extension (opt: Option[T])
def response: Response =
opt match
case None => Response.status(404).build()
case Some(t) => summon[HttpResponse[T]].response(t)

The method summon in Predef returns the given of a specific type.

The usage remain similar to what it was previously.

import HttpResponseLike.stringResponse"all is good".response 
//Response{status=200, reason=OK}
val someValue: Option[String] = Some("all is good")
import ResponseApp.HttpResponseLike.optHttpResponse
someValue.response
//Response{status=200, reason=OK}

And that is it!! We have defined a type class and implemented it for concrete type String and Option. We saw in Scala 3 a type class’s implementation is expressed through a given instance definition, which is supplied as an implicit argument alongside the value it acts upon. The type class solution takes more effort to set up, but is more extensible: instances for type classes can be defined anywhere.

Further references :

  1. https://dotty.epfl.ch/docs/reference/contextual/type-classes.html

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store