Implicits in Scala aim to make your code simpler and more concise by abstracting away some of the details of your implementation into some implicitly-resolved utilities. This keeps your concerns separated, keeping low-level logic like serialisation or the specifics of accessing data structures away from higher-level logic.
Today we'll take a look at how to write implicits, how they're resolved at compile time, and good and bad use cases for them.
There are a few types of implicits in Scala, and they all work at compile time, by providing the compiler with a means of resolving what would otherwise be a type error or missing argument.
The most straightforward implicits to understand are implicit argument lists in a function or class. Consider the following, a very common use case for implicits:
class FishRepository(host: String)(implicit ec: ExecutionContext) {
def findOne(id: String): Future[Fish] = ???
}
Here we define a repository which deals with fetching records about scaly sea creatures from a database.
Since our repository will work with concurrency using Future
s, it's going to need to know which
ExecutionContext
to use. We want that to be configurable further up the stack, since we might need to
tweak our execution contexts based on load in different parts of the application, and most likely want
database IO to run on a dedicated future pool.
To save us the effort of passing in an explicit ExecutionContext
everywhere it may be needed throughout
the stack, we simply declare it an implicit argument and allow it to be resolved automatically.
implicit val ec: ExecutionContext = ExecutionContext.global
val fisher = new FishRepository("localhost") // implicit ec is filled in for us
To fill in these implicit arguments, the compiler will do the following:
implicit
with type ExecutionContext
It's important to note that point (3) here means you have to be careful with scoping your implicits, and with which types are declared as implicit arguments. If an implicit can be fulfilled in multiple ways it will simply fail, and you'll have to either change the scope of your implicits or provide the arguments explicitly instead. In general they should be very specific types, to avoid confusion or ambiguity about what is expected.
If you defined an implicit String
argument, for example, it could mean anything – a name, a hoststring, a
password, a type of root vegetable. An implicit Int
might be a buffer size, a rate limit, a concurrency
limit, or all of the above. To avoid this, use a specific type, creating one if needbe – e.g.
case class Parallelism(val value: Int)
is one I've used in the past to encapsulate the parallelism setting
desired in akka stream components.
Implicit classes are typically used in Scala to provide type class functionality. This is a means of achieving "ad hoc polymorphism" – adding a capability to a type on the fly, and without needing to modify or have any control over the type in question.
An excellent example of type classes in Scala, and one which I touched upon last week, is adding JSON serialisation functionality to your models. Here's a simplified example which acts as CSV encoding behaviour, currently for a single case class:
case class Fish(species: String, age: Int, endangered: Boolean)
implicit class FishEncoder(fish: Fish) {
def toCsv: String = Seq(fish.species, fish.age, fish.endangered).mkString(",")
}
This allows us to treat toCsv
as a method of Fish
, even though it's not defined on the class:
> Fish("Salmon", 12, false).toCsv
res1: String = "Salmon,12,false"
Defining a type class to a single specific type like this isn't very helpful in the case of a JSON or CSV encoder, so we'll revisit this example shortly to make it more generic.
Here's what the compiler is doing in this case:
toCsv
defined on Fish
or one of its parentsFish
which might provide oneFishEncoder
, instantiate it with our Fish
, and call toCsv
on this instance insteadFirstly, note that implicit conversions are no longer recommended for use, and you should use implicit
classes to achieve the same thing instead. In fact, standard library tooling like the implicit
conversion-based scala.concurrent.JavaConversions
was deprecated in favour of the implicit class-based
scala.concurrent.JavaConverters
several versions ago, and since removed entirely in 2.13.
Implicit conversions convert one type to another using an implicit function. These are the "most magical" types of implicits in that the process is entirely implicit – unlike implicit classes, where you call a method, hinting that there's an implicit class involved if it's not defined on the class itself. With implicit conversions, you can simply use the source type as if it were the target type.
Consider the following:
import scala.language.implicitConversions
case class Fish(species: String, age: Int, endangered: Boolean)
case class FishSummary(fishCount: Int, hasEndangered: Boolean)
implicit def fishToSummary(fish: Seq[Fish]): FishSummary = {
FishSummary(fish.length, fish.exists { _.endangered })
}
With the implicit conversion defined and in scope, we can simply treat a Seq[Fish]
as if it were a
FishSummary
:
> val summary: FishSummary = Seq(Fish("Salmon", 12, false), Fish("Atlantic Halibut", 9, true))
summary: FishSummary = FishSummary(2, true)
// --- or, pass it into a function which expects a FishSummary, like:
def printSummary(summary: FishSummary): Unit = print(summary)
> printSummary(Seq(Fish("Salmon", 12, false), Fish("Atlantic Halibut", 9, true)))
FishSummary(2,true)
This time, the compiler resolves the implicit as follows:
Seq[Fish]
conforms to type FishSummary
Seq[Fish]
and
produces FishSummary
FishSummary
But, to reiterate: implicit conversions are no longer recommended since they're too prone to be
confusing: since version 2.10, the compiler will produce a warning if you define them. You can acknowledge
this and switch off the warning by importing scala.language.implicitConversions
as shown above. In
extremely narrow use cases a locally-scoped implicit conversion may produce a small readability benefit, so
this will allow you to use them without warnings if you really need one.
To perform the same operation shown above using an implicit class, you could use:
implicit class RichFishSeq(fish: Seq[Fish]) {
def toSummary: FishSummary = FishSummary(fish.length, fish.exists { _.endangered })
}
val summary: FishSummary = {
Seq(Fish("Salmon", 12, false), Fish("Atlantic Halibut", 9, true)).toSummary
}
Note that this is, regardless, quite a contrived example, and likely too specific to be a good use case for an implicit at all. A simple utility function to create the summary is likely to be much easier to understand in this case. Exercise restraint in deciding when to use an implicit.
For implicits to work, they have to be in scope when they're used. Given that the purpose of implicits is to make your code concise and readable and avoid mixing layers of logic, you'll likely want to package them separately and reusably.
Typically, implicits are packaged like this:
trait FishImplicits {
implicit class RichFishSeq(fish: Seq[Fish]) {
def toSummary: FishSummary = ???
}
// More fish-related puns / converters
}
object FishImplicits extends FishImplicits
This is a standard pattern you'll see in many libraries, as it allows you to use either
import FishImplicits._
or extends FishImplicits
to bring your implicits into scope. This gives you some
flexibility to compose traits to collect together implicits, or limit their scope by importing. This, in
turn, allows you to control the exact scope of your implicits to minimise confusion.
Note also that we've also used Implicits
in the name, making it very clear that we're bringing in some
implicits. The name may be slightly more specific where appropriate, e.g. Spray JSON implicits are defined in
XXXJsonProtocol
traits, and the standard library scala-to-java collection converters are called
AsScalaConverters
. In all cases we aim for clarity that implicits are involved and that we're effectively
bringing in a new DSL. This gives us a heads up of where to look if we're experiencing unexpected behaviour
or wondering about unexpected syntax, as unannounced implicits can cause real pain.
Finally, a specific call-out: for all of the reasons above, do not be tempted to place implicits in the package object. Package objects are generally best avoided entirely, as they can make scoping confusing regardless, but that's further compounded when you're using implicits. Implicits should always involve a clear import as a heads up that they're present.
Implicit classes become even more powerful when they're combined with generic types, as I hinted at earlier
when describing how a typical JSON (or CSV) library might work. A full discussion of this is best left for
another day, but to complete our earlier thought, let's have a quick look at how a typical JSON library
works, by thinking about how our toCsv
API might be completed for generic types:
// We need to provide a CsvEncoding subclass for each type we want to encode to CSV
trait CsvEncoding[T] {
def encode(t: T): String
}
// The core of the CSV encoder implicits; this defines any type as encodable to CSV
// if an implicit CsvEncoding is available, using the toCsv method
trait CsvImplicits {
implicit class CsvEncodable[T : CsvEncoding](t: T) {
def toCsv: String = implicitly[CsvEncoding[T]].encode(t)
}
}
// Our type-specific encodings
trait FishCsvEncodings {
implicit object FishCsvEncoding extends CsvEncoding[Fish] {
override def encode(fish: Fish): String = {
Seq(fish.species, fish.age, fish.endangered).mkString(",")
}
}
}
// Finally, just bring both the CSV implicits and our fish CSV encodings into scope
object App extends CsvImplicits with FishCsvEncodings {
val fishy: String = Fish("trout", 11, false).toCsv
}
This works by defining a CsvEncodable
type which will work for any type which has an implicit CsvEncoding
available. The .toCsv
behaviour comes from CsvEncodable
, and it falls back on the definition you've
provided via a CsvEncoding
instance. Bring both the implicit class and your set of implicit encoders into
scope, and the API works as expected.
Two things to clear up:
[T : CsvEncoding]
is a syntactic sugar which requires a CsvEncoding[T]
to be in scopeimplicitly[T]
is a function which takes an implicit T
and simply returns it; it's used to
refer to the implicit instance without having a name for itWith that sugar removed, this is defined as:
implicit class CsvEncodable[T](t: T)(implicit ev: CsvEncoding[T]) {
def toCsv: String = ev.encode(t)
}
There are a few ways which this could all be improved, of course, as our CSV "encoder" is very naive. We may
also consider an additional DSL to add toCsvString
to types to determine how individual fields should be
converted to strings before inserting into the CSV, rather than assuming calling .toString
on them produces
the desired result. I leave it to the reader to think about other improvements we could make here.
Since I've touched a few times on some potential pitfalls of using implicits poorly, with too-weak types, poor scoping, or simply overzealousness, I'd like to share a horror story from a codebase I had to work with several years ago.
The codebase in question was a large monolithic application which generally had a lot of problems, including
very weak types used, frequent use of Map[String, Any]
and similar instead of case classes, large numbers
of fields combined in difficult-to-follow ways, a 55-minute build time, very difficult to parse logs, poor
test coverage, and a structure which made unit testing difficult.
All of this contributed to slow development, but one particular implicit incident is engraved in my memory.
I first stumbled into this by attempting to call a method like:
def store(id: ObjectId, title: String, encoding: String, date: Instant): Future[Unit]
like this:
repository.store(title, id, encoding, Instant.now())
If you're keen-eyed, you'll see that I've transposed the first two arguments. This is often a drawback of
having too-weak types, and if both were String
s I'd likely only find this out during a test run, or in this
application, by carefully scrutinising logs or database records to find that my ID was wrong. But with one
argument having type ObjectId
this simply won't compile, and I'll immediately see the problem. Right?
Wrong.
Enter an implicit conversion, defined in the package object, in a package with 100+ classes, hidden from sight:
implicit def any2ObjectId(a: Any): ObjectId = a match {
case id: ObjectId => id
case str: String => ObjectId(str)
case _ => throw new Exception("Bad object ID")
}
Unhelpfully, this broad implicit was trying to correct my compile error by forcing the type to ObjectId
if
I provided a String
, or anything else for that matter. Combined with other issues in the codebase, the
resulting camouflaged error took a staggering two weeks to identify.
If you're keen-eyed again, you may also notice that the first branch of the match
here is unreachable:
the compiler will not reach for the implicit conversion if it already has an ObjectId
, so this could not be
called with one. With that in mind, I'd like to also give an honourable mention to another implicit I found
alongside it:
implicit def any2Any(a: Any): Any = a match {
case id: ObjectId => id
case str: String => ObjectId(str)
case _ => throw new Exception("Bad object ID")
}
...which, thankfully, does nothing at all.
We collectively resolved to eliminate these implicits, which was a long process, as deleting some of them spawned several hundred compile errors all across a monolith a dozen developers were actively working on.
An executive summary: