Using Scala implicits
Implicits is a scala feature that often gets a bad rap. Like any tool, it can be both used AND abused. I’m convinced if you know how and when to apply them your scala code will look and feel cleaner. Here are some use cases, mostly taken from Programming Scala, 2nd Edition
Type Class Pattern - Adding behavior to types
This is a convenient way to add methods to any type, also known as ad hoc polymorphism
Suppose I am constructing an application to collect census data and I want to write each of the following models to separate CSV files (comma separated values).
case class Household(address: String, numOccupants: Int)
case class Person(fullName: String, age: Int, homeAddress: String)
How can I convert Household
and Person
to strings of comma separated values?
Without using implicits, I might define top level functions:
// Convert a household to its CSV representation
def toCSVRepr(household: Household): String =
household.addres + "," + household.numOccupants.toString
// Convert a Person to their CSV representation
def toCSVRepr(person: Person): String =
person.fullName + "," + person.age.toString + "," + person.homeAdress
val household = Household("1 Strawberry St", 5)
val person = Person("Dalton Sweeney", 22, "1 Strawberry St")
val householdCSVRepr = toCSVRepr(household)
val personCSVRepr = toCSVRepr(person)
This is fine but we might prefer to be able to call toCSVRepr
directly on
instances of Household
and Person
. We can use implicits for this.
object Implicits {
trait ToCSVRepr {
def toCSVRepr: String
}
implicit class HouseholdToCSVRepr(household: Household) extends ToCSVRepr {
def toCSVRepr: String = household.addres + "," + household.numOccupants.toString
}
implicit class PersonToCSVRepr(person: Person) extends ToCSVRepr {
def toCSVRepr: String = person.fullName + "," + person.age.toString + "," + person.homeAdress
}
}
import Implicits._
val householdCSVRepr = Household("1 Strawberry St", 5).toCSVRepr
val personCSVRepr = Person("Dalton Sweeney", 22, "1 Strawberry St").toCSVRepr
How can I call toCSVRepr
on instances of Household
and Person
when it’s
not defined on those classes? Here’s what’s happening under the hood:
- At
val householdToCSVRepr = ...
, the compiler notices thattoCSVRepr
is not defined onHousehold
- It searches for any definition of
toCSVRepr
in scope. - If there is a
toCSVRepr
definition in scope and it’s wrapped in an implicit class that can be instantiated from an instance ofHousehold
, then we’re good to go.HouseholdToCSVRepr
satisfies these requirements. Household("1 Strawberry St", 5)
is implicitly substituted withHouseholdToCSVRepr(Household("1 Strawberry St", 5))
.
Very cool! The implicit classes HouseholdToCSVRepr
and PersonToCSVRepr
are
known as type classes
It’s also convenient to wrap your type classes in an Implicits
object, that way they can
be imported only exactly where they are needed. You don’t want implicits running
around your project willy-nilly!
The object oriented alternative
Rather than using type classes, we could have had both Household
and Person
extend from a trait defining toCSVRepr
:
sealed trait Model {
def toCSVRepr: String
}
case class Household(...) extends Model {
override def toCSVRepr: String = ...
}
case class Person(...) extends Model {
override def toCSVRepr: String = ...
}
There are several reasons you might not want this, depending on your use case.
The benefits of type classes over inheritance
Sometimes the classes on which you want to define your common method have no obvious shared hierarchy and it doesn’t make sense to have them extend from the same base class/trait. Creating a common superclass can seem forced and unreasonable.
Secondly, there is one great advantage to type classes over inheritance when designing API’s used by clients. Suppose a API/feature is requested by a small but important subset of clients.
If you were to add methods for this new feature to a base class and force all subclasses to implement them, this would burden all clients with the new methods. Your new methods are exposed to a lot of clients that don’t need or care about them!
Using the type class pattern and ad hoc polymorphism, these new methods could be
defined as implicit classes in an Implicits
object/package. Clients who need the new feature can decide to import this new object/package. The API doesn’t change at all for the other clients and everyone is happy!
Constrain the types that can be applied to a function
Now that we’ve successfully converted households and persons to CSV
representation using ad hoc polymorphism we need a way of converting them back
to instances of their respective classes. In this section I’ll show you how
implicits can help us in going from the raw data representation (CSV) to the
model representation (Household
and Person
).
Without implicits it might look something like this:
def getHousehold(csvRepr: String): Household = {
val parts = csvRepr.split(",")
val address = parts(0)
val numOccupants = parts(1).toInt
Household(address, numOccupants)
}
def getPerson(csvRepr: String): Person = {
val parts = csvRepr.split(",")
val fullName = parts(0)
val age = parts(1).toInt
val homeAddress = parts(2)
Person(fullName, age, homeAddress)
}
val householdRepr: String = ...
val personRepr: String = ...
val household = getHousehold(householdRepr)
val person = getPerson(personRepr)
With implicits, we can define an implicit class with a parameterized method:
implicit class ModelAsCSV(csvRepr: String) {
def get[T](implicit val toT: String => T): T = toT(csvRepr)
}
Now we’ll be able to call
val household = householdRepr.get[Household]
val person = personRepr.get[Person]
As long as we implement implicit values for String => Household
and String => Person
implicit val csvReprToHousehold: String => Household = (s: String) => {
val parts = s split ","
val address = parts(0)
val numOccupants = parts(1).toInt
Household(address, numOccupants)
}
implicit val csvReprToPerson: String => Person = (s: String) => {
val parts = s split ","
val fullName = parts(0)
val age = parts(1).toInt
val homeAddress = parts(2)
Person(fullName, age, homeAddress)
}
Voila! get[T]
is only available for types T
that have an implicit String => T
in scope.
Passing around contexts, connections, and sessions, and more
Execution contexts, database connects, authentication sessions, and more. What do they have in common? They often act as boilerplate parameters to a lot of methods. It would be nice if we could avoid writing them altogether for most method calls. Implicit method parameters allow us to do this.
Scala Futures
Execution contexts are what futures use to execute the function passed to their
apply
method. We are often content with using the default global execution
context:
import ExecutionContext.Implicits.Global
val f = Future[String] {
"Hello from the future!"
}
is equivalent to
val ec: ExecutionContext = ...
val f = Future[String] {
"Hello from the future!"
}(ec)
It may not seem like much now, but in a highly concurrent application that uses futures everywhere it avoids a lot of writing and makes reading code easier.
Other objects
You could imagine similar use cases for objects like database connections, thread pools, and authentication sessions.
Conclusion
- If you are passing around a lot of boilerplate method parameters, implicit parameters are your friend.
- If you need ad hoc polymorphism and don’t think type inheritance is the right choice, use the technique of implicit classes outlined in the article.
- If you need to constrain the allowed types on a method, use the implicit classes with implicit val technique outlined in the article.
There is another technique for constraining allowed types called implicit evidence that I omitted in this article for the sake of keeping it simple. It is less widely used, but read up if you’re curious.
Thanks for reading!