11 minutes read

In some situations, there is an execution context in the code. It has to be passed to functions again and again, which makes the code overloaded.

Let's imagine that we have a server that processes client requests. We want to display the stages of processing on the console, but in order to distinguish customers, it is necessary to print some additional information:

def log(message: String, context: String): Unit =
  println(s"${System.currentTimeMillis()}: $message, context: {$context}")

val clientMetadata: String = "client-name: Max, client-id: 41920"

log("Client verified", clientMetadata)
log("Request processing", clientMetadata)
log("Request completed successfully", clientMetadata)

Let's look at how Scala can help us simplify this code using implicit parameters and syntactic sugar!

This topic is related to Scala 2. In Scala 3 we can still use these constructs, but they are deprecated in favor of given/using

Syntax

There is a keyword in Scala called implicit. We can specify it before the function parameter to show that it can be passed implicitly:

def log(message: String)(implicit context: String): Unit =
  println(s"${System.currentTimeMillis()}: $message, context: {$context}")

But how can this parameter be passed to a function? You can do this explicitly, as shown here:

val clientMetadata: String = "client-name: Max, client-id: 41920"

log("Client verified")(clientMetadata)

If you specify implicit during the creation of a variable, it will be substituted into the function automatically, so there is no need to specify the argument:

implicit val clientMetadata: String = "client-name: Max, client-id: 41920"

log("Client verified")
log("Request processing")
log("Request completed successfully")

We can also make several parameters implicit. The word implicit will affect the entire list of function parameters, which we have enclosed in brackets:

def log(message: String)(implicit clientName: String, clientId: Long): Unit =
  println(s"${System.currentTimeMillis()}: $message, context: {client-name: $clientName, client-id: $clientId}")

implicit val clientName: String = "Max"
implicit val clientId: Long = 41920

log("Client verified")
log("Request processing")
log("Request completed successfully")

It is important to note that explicit and implicit parameters should always be separated. The following code will be incorrect:

def incorrectLog(message: String, implicit context: String): Unit // <- doesn't compile

Path of implicit resolving

As you have just seen, the language allows you to automatically substitute parameters into functions. But how does Scala understand what exactly needs to be passed? Where can these parameters be taken from?

The compiler first looks for implicit parameters inside the current code block. Usually, the block is enclosed is parentheses: { ... }. If the implicit parameter is not found, the search will continue in the code that does not require additional import.

In the following example, there are two implicit contexts. The context will be selected from the variable space:

{
  implicit val context: String = "Common code block"

  val unit = {
    implicit val context: String = "Variable code block" // <- will be selected
    log("Message")
  }
}

If you remove an implicit variable from the variable block, a variable from the common space will be selected:

{
  implicit val context: String = "Common code block" // <- will be selected

  val unit = {
    log("Launching the application")
  }
}

It is important to understand that the compiler cannot choose for us. If Scala detects several implicit parameters in one space that will match the type, the code will not compile. In the following example, we will see the Ambiguous implicit values error:

val unit = {
  implicit val context1: String = "First"
  implicit val context2: String = "Second"
  log("Launching the application") // <- doesn't compile
}

If the compiler cannot find the desired implicit parameter, it will just report an error: could not find implicit value.

Implicit conversions

Great news: not only parameters but the whole functions can be made implicit!

Let's recall our function that prints a message. We'll define the context as empty: it does not make any difference at the moment.

def log(message: String)(implicit context: String): Unit =
  println(s"${System.currentTimeMillis()}: $message, context{$context}")

implicit val emptyContext: String = ""

We can't pass a tuple of values to it — the type doesn't match:

log((1L, "Start")) // <- doesn't compile

We can write a conversion from (Long, String) to String and add the implicit keyword. The compiler will understand that this function can be used implicitly when we are trying to pass a tuple in a field with a String type:

implicit def pairToString(in: (Long, String)): String = s"Process ${in._1}, ${in._2}"

log((1L, "Start")) // it actually works like this: log(pairToString((1L, "Start")))

The search for implicit conversions is similar to the search for variables. For example, if we define two functions, the compiler will throw an error:

implicit def pairToString(in: (Long, String)): String = s"Process ${in._1}, ${in._2}"
implicit def pairToStringSimple(in: (Long, String)): String = in.toString

log((1L, "Start")) // <- doesn't compile

Be careful

Implicit conversions and parameters can often come in handy. But with great power comes great responsibility!

If you use them too often, the code may lose readability. The last two lines in the example may be misleading, and even contain an error:

def createRequest(implicit clientName: String, clientId: Long): String = s"{id:$clientId, name:$clientName}"
def sendRequest(implicit request: String, requestId: Int): Unit =
  println(s"id: $requestId, request: $request")

implicit val clientName: String = "Max"
implicit val clientId: Long = 42L
implicit val requestId: Int = 1

val request = createRequest // Is this a function call?
sendRequest // Oops, instead of a request, we send the client's name!

Try to use implicit only in cases when there is no direct connection to the logic of the program.

If you work in IntelliJ IDEA, you can use the View > Show Implicit Hints function so that the editor displays all the parameters and conversions:

a screenshot of the location of the show implicit hints function

a screenshot of the editor showing all parameters and conversions

Conclusion

Great! You've learned how the implicit keyword works in Scala, how to use implicit parameters and conversions, and also have an idea of how the compiler finds and substitutes values. Now you are well-equipped for solving problems!

5 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo