Scala implicit for mere mortals (like me)

Sharing is caring!

One of the most difficult feature to master in Scala (at least for mere mortals of course) is the implicit keyword and its implications especially because it may appear in different places and for different purposes:

  • Implicit methods
  • Implicit parameters
  • Implicit variables
  • Implicit classes
  • Implicit constructors

So what is it useful for?
Implicits are essentially a way to leverage the power of Scala compiler by avoiding (otherwise) explicit calls to methods in our code or allowing us to extend native or third party classes “dynamically”.

Implicit methods

Implicits are used by Scala itself, for example: have you ever wonder why:

1
val value: Double = 1 // <- this is an Int not a Double!

compiles without any issue and returns the expected value (1.0)? Implicit in action!
If we look at scala.Int companion object we will find the following implicit method declaration:

1
implicit def int2double(x: Int): Double = x.toDouble

So, at compile-time, once the compiler finds a type mismatch in the code that will generate an exception (in this case an Int instead of a Double), it will check for an implicit in the current scope that is able to convert the wrong type into the correct one. If found, the compiler will replace the code by invoking the method for us:

1
val value: Double = int2double(1) // <- explicit replacement

Let’s try to do a similar magic by ourselves. Let’s make the compiler be able to understand booleans as 'yes or 'no symbols:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
object MyApp {
  implicit def symbolToBoolean(symbol: Symbol): Boolean = {
    symbol match {
      case 'yes => true
      case 'no => false
    }
  }
  def main(args: Array[String]): Unit = {
    val scalaIsCool: Boolean = 'yes
    val scalaIsEasy: Boolean = 'no
    println(s"scalaIsCool: $scalaIsCool")
    println(s"scalaIsEasy: $scalaIsEasy")
  }
}

So, once the compiler encounters the following assignment:

1
val scalaIsCool: Boolean = 'yes

Since a boolean can be only true or false, it will check for a way to give this symbol a boolean value, so it proceeds by checking the current scope (main() method), here there are no implicit definitions, so it goes up to the parent scope (MyApp), here it finds an implicit that is able to convert the symbol, so it will replace the code with:

1
val scalaIsCool: Boolean = symbolToBoolean('yes)

and the same for the next line.
Of course this is just a dumb example and in the real life implicits are usually defined in separated classes.
We could rewrite the example as:

SymbolImplicits.scala

1
2
3
4
5
6
7
8
9
10
object SymbolImplicits {
  implicit def symbolToBoolean(symbol: Symbol): Boolean = {
    symbol match {
      case 'yes => true
      case 'no => false
    }
  }
}

AppImportingImplicitMethods.scala

1
2
3
4
5
6
7
8
9
10
11
12
13
import SymbolImplicits._
object MyApp {
  def main(args: Array[String]): Unit = {
    val scalaIsCool: Boolean = 'yes
    val scalaIsEasy: Boolean = 'no
    println(s"scalaIsCool: $scalaIsCool")
    println(s"scalaIsEasy: $scalaIsEasy")
  }
}

Implicit classes

One of the best use case for Scala implicit is to extend native or third party classes. Let’s pretend for example that we need to turn an arbitrary String into a slug, we could create an object like StringUtils with a method slufigy(string: String) but wouldn’t be much more clean to be able to call .toSlug against a String instance as we usually do with .toUpperCase and so on? Of course, and by defining an implicit class we can have such convenience:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
object StringExtensions {
  implicit class StringWrapper(string: String) {
    def toSlug: String = {
      string
        .toLowerCase
        .replaceAll("[^a-z\\d]", " ")
        .trim
        .replaceAll("\\s+", "-")
    }
  }
}

Then later on we can:

1
2
3
4
5
6
7
8
9
import StringExtensions._
object MyApp {
  def main(args: Array[String]): Unit = {
    println(">> This string will be slugified! <<".toSlug)
  }
}

Bear in mind that an implicit class cannot be defined as a top-level object, therefore it must be “wrapped” by an object!

Implicit parameters

Also method parameters can be defined as implicit thus providing a sort of “native dependency injection”.
Let’s create a sample class which is responsible of creating a folder tree by naming folders according to the current date (/yyyy/MM/dd/):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
object FoldersCreator {
  def createDateFolderTree(root: Path)(implicit now: LocalDateTime): Option[Path] = {
    val format = (p: String) => now.format(DateTimeFormatter.ofPattern(p))
    val path: Path = Paths.get(root.toString, format("yyyy"), format("MM"), format("dd"))
    val result: Option[Path] = Try(Files.createDirectories(path)).toOption
    result
  }
  def createDateFolderTree(root: String)(implicit now: LocalDateTime): Option[Path] = {
    createDateFolderTree(Paths.get(root))
  }
}

As you can see I defined a method with an overload and both have a secondary couple of parenthesis containing a parameter now declared as implicit. This means that we will be able to call the methods without the second argument:

1
FoldersCreator.createDateFolderTree("a/root/path")

Implicit variables

…provided that in the scope in which they get invoked an implicit variable of type LocalDateTime is found (the name doesn’t matter!).
Moreover as you can see even in the inner call from the overloaded method (createDateFolderTree(rootPath: String)...) to the original signature (createDateFolderTree(rootPath: Path)...) the second argument is omitted.
Let’s see how we can use the previously created object with implicit arguments:

1
2
3
4
5
6
7
8
9
object MyApp {
  def main(args: Array[String]): Unit = {
    implicit val now: LocalDateTime = LocalDateTime.now()
    FoldersCreator.createDateFolderTree("/home/dave/Desktop")
    FoldersCreator.createDateFolderTree("/home/dave/Desktop/backup")
  }
}

In the code above the implicit variable has been defined just before invoking methods and it has been named like the argument in the signature (now).
However the implicit date can be declared also as class field and with a different name:

1
2
3
4
5
6
7
8
9
10
object MyApp {
  private implicit val d: LocalDateTime = LocalDateTime.now()
  def main(args: Array[String]): Unit = {
    FoldersCreator.createDateFolderTree("/home/dave/Desktop")
    FoldersCreator.createDateFolderTree("/home/dave/Desktop/backup")
  }
}

If we define several implicit val, we may face a resolution exception. For example if we add a new implicit val of the same type (LocalDateTime) to the previous code:

1
2
3
4
5
6
7
8
9
10
11
object MyApp {
  private implicit val date: LocalDateTime = LocalDateTime.now()
  private implicit val now: LocalDateTime = LocalDateTime.of(2020, 1, 1, 20, 30)
  def main(args: Array[String]): Unit = {
    FoldersCreator.createDateFolderTree("/home/dave/Desktop")
    FoldersCreator.createDateFolderTree("/home/dave/Desktop/backup")
  }
}

We will get:

1
2
3
4
Error:(11, 40) ambiguous implicit values:
 both value date in object AppWithImplicitsVariables of type => java.time.LocalDateTime
 and value now in object AppWithImplicitsVariables of type => java.time.LocalDateTime
 match expected type java.time.LocalDateTime

This is a further proof that variable names don’t matter, only type does and if two variables with the same type are found in the scope the compiler doesn’t know which one to pick up.
Obviously in this case the problem is immediately spottable, but in a real application it might be harder to discover where is the conflict, especially if there are many classes with many implicits, generics and so on.

Implicit constructors

Like methods even constructor can be composed by implicit arguments.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// an "injectable" object
class Bar
// a class with the implicit
class Foo(implicit val bar: Bar)
// tests app
object FooBarApp {
  private implicit val bar: Bar = new Bar
  def main(args: Array[String]): Unit = {
    val foo: Foo = new Foo
    println(foo.bar.getClass.getName)
  }
}

Final thoughts

The examples that I provided in this post are intentionally dumb and short, however with Scala implicit is possible to achieve complex tasks and automatically data conversion. For example Spark use this feature quite heavily for data encoding. Anyway this powerful tool should be used carefully and not abused in order to avoid painful situations.

shares