How Do Implicits Work in Scala?

Programmers strive to write simple and understandable code. The less code is written, the less likely it is that there is an error in it. Scala offers the ability to write even more concise code and rely on the help of the compiler. This is achieved through implicit conversions and implicit parameters. However, everything that is implicit, usually brings only misunderstanding. Let’s see what lies behind the magic of implicits in Scala.

Implicit conversions

def call(str: String): Unit = println(str)implicit def intToString(i: Int): String = i.toStringcall(1)

Let’s see how scalac understands this code (here and throughout -Xprint:typer key is used for the compiler):

object ImplicitApp extends Object with App {
def call(str: String): Unit = scala.Predef.println(str);
implicit def intToString(i: Int): String = i.toString();
ImplicitApp.this.call(ImplicitApp.this.intToString(1))
}

Indeed, the compiler adds another method to the chain of calls, so that the type of the passed parameter in the call method matches the required one.

Such way of using Scala’s implicit conversion is the most non-obvious. Only IDE and hours spent on debugging will let you find out why the call method works with the parameter of the wrong type. Do not abuse this kind of implicit conversions.

Extension methods

implicit class RichString(str: String) {
def awesomeMethod(): Unit =
println(s"awesomeMethod for $str")
}
"string".awesomeMethod()

Or, by using an implicit function that creates an anonymous class with the necessary methods:

implicit def richString(str: String) = new {
def awesomeMethod(): Unit =
println("awesomeMethod")
}
"string".awesomeMethod()

Under the hood, the following happens (with an anonymous class function as an example):

object MethodExtension extends AnyRef with App {
implicit def richString(str: String): AnyRef{def awesomeMethod(): Unit} = {
final class $anon extends scala.AnyRef {
def awesomeMethod(): Unit =
scala.Predef.println("awesomeMethod")
};
new $anon()
};
MethodExtension.this.richString("string").awesomeMethod()
}

As can be seen, before calling the “non-existent” method, a new object is created and this method is called on it. The presence of a specific method (albeit, at first glance, it is unclear where it came from) makes the code more readable and maintainable.

Implicit parameters

implicit val executor: Executor = (task: Task) => println(task.toString)def run(task: Task)(implicit executor: Executor): Unit = executor.run(task)run(new Task {})

The algorithm of actions is the same: the compiler finds in the scope the implicit with the needed type and passes it instead of us to the function:

object ImplicitParameter extends AnyRef with App {
private[this] val executor: Executor =
((task: Task) => scala.Predef.println(task.toString()));

implicit <stable> <accessor> def executor: Executor =
ImplicitParameter.this.executor;

def run(task: Task)(implicit executor: Executor): Unit = executor.run(task);
ImplicitParameter.this.run({
final class $anon extends AnyRef with Task {};
new $anon()
})(ImplicitParameter.this.executor)
};

Thus, we simplify the mandatory part of the signature function, and the ability to deal with the implicit is handed over to the compiler.

Type classes

trait Equal[A] {
def equal(a1: A, a2: A): Boolean
}
object Equal {
def apply[A](implicit instance: Equal[A]): Equal[A] = instance
implicit class EqualSyntax[A](a: A) {
def equal(that: A)(implicit e: Equal[A]): Boolean =
e.equal(a, that)
}
}

Equal can be used directly (apply method):

implicit val intEqual: Equal[Int] =
(a1: Int, a2: Int) => a1 == a2
println(Equal[Int].equal(1, 2))

and through the extension method that adds a method for comparison to any class (if there are corresponding implicits):

import Equal.EqualSyntaxprintln(1 equal 2)

Since we have already gone over the approaches used in the last two examples, there are no surprises for us in the compiler’s actions:

object TypeClass extends AnyRef with App {
private[this] val intEqual: Equal[Int] =
((a1: Int, a2: Int) => a1.==(a2));
implicit <stable> <accessor> def intEqual: Equal[Int] =
TypeClass.this.intEqual;
scala.Predef.println(Equal.apply[Int](TypeClass.this.intEqual).equal(1, 2)); // substitution of an implicit parameter

import Equal.EqualSyntax;
scala.Predef.println(Equal.EqualSyntax[Int](1).equal(2)(TypeClass.this.intEqual)) // implicit conversion to the required typeclass
}

Instead of conclusion, here are some truths from Captain Obvious:

  • no magic of implicits in Scala exists;
  • to avoid dealing with questions about where this or that implicit has come from, adhere to agreements on their placement and usage in your project.

The source code for the examples is available on Github.

Originally published at anadea.info

We build software and we do it right | Custom web and mobile development | https://anadea.info/