Computer scienceProgramming languagesScalaControl flowFunctions

Given/using/conversion

9 minutes read

In certain situations, the code has an execution context. This has to be passed to functions repeatedly, which makes the code overloaded.

Imagine we have a server processing client requests. We want to display the stages of processing on the console, but to differentiate between customers, it's 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 see how Scala can help us simplify this code using implicit parameters and syntactic sugar!

Syntax

In Scala 3 we have two keywords: given, which provides an implicit context, and using, which uses an implicit context.

The using keyword is used to explicitly provide given instances as implicit parameters when invoking methods or constructors. It indicates that a given instance should be used to fulfill the implicit parameter requirement.

Let's denote in our function that context can be passed implicitly:

def log(message: String)(using 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")(using clientMetadata)

The given keyword is used specifically to declare given instances, which serve as providers of implicit values or conversions. When you define an implicit value using the given keyword, it can be automatically passed as an implicit argument to a function or method that requires it:

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

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

Also, given instances can be defined without being explicitly named. Instead, you can directly assign a value to the given instance:

given String = "client-name: Max, client-id: 41920"

If you have complex instances that require custom implementations, you can use the given with syntax. This syntax provides a concise way to define a given instance and its implementation in a single expression:

trait Show[A]:
  def show(a: A): String


object Show:
  given showInt: Show[Int] with
    def show(a: Int): String = s"Int: $a"

The name of a given can be left out:

object Show:
  given Show[Int] with
    def show(a: Int): String = s"Int: $a"

And then you can use this instance as:

def printWithShow[A](a: A)(using showInstance: Show[A]): Unit =
  val str = showInstance.show(a)
  println(str)  // Int: 42

In Scala 3, you can define a given instance using an alias, which allows you to assign a given instance to an expression or value:

given executor: ExecutionContext = ForkJoinPool()

In addition, you can utilize multiple implicit values within your functions:

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

given clientName: String = "Max"
given 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, using context: String): Unit // <- doesn't compile

Path of implicit resolution

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

The compiler first looks for implicit parameters inside the current code block. Usually, the block is enclosed is parentheses: { ... }. If the implicit parameter isn't found, the search continues in the code that doesn't require additional import.

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

{
  given context: String = "Common code block"

  val unit = {
    given 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:

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

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

It is important to understand that the compiler can't choose for us. If Scala detects several implicit parameters in one space that match the type, the code won't compile. In the following example, we'll see the ambiguous given instances error:

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

If the compiler can't find the desired implicit parameter, it will report an error: no given instance of type String.

Implicit conversions

Let's revisit our function's message parameter.

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

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

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

But if you're sure that in your application (Long, String) can always be converted to String, you can write a conversion using the given keyword and the Conversion class. The compiler will understand that this conversion can be used implicitly when we're trying to pass a tuple in a field with a String type:

given Conversion[(Long, String), String] with
  def apply(in: (Long, String)): String = s"Process ${in._1}, ${in._2}"

log((1L, "Start")) // The implicit conversion is applied automatically

The search for conversions is similar to the search for values. For example, if we define two of them, the compiler will throw an error:

given pairToString: Conversion[(Long, String), String] = ???
given pairToStringSimple: Conversion[(Long, String), String] = ???

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 frequently, the code may become less readable. The last two lines in the example may be misleading, and even contain an error:

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

given String = "Max"
given Long = 42L
given 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 given/using and Conversion only in situations where they don't directly affect the program's logic.

If you're using IntelliJ IDEA, you can enable the View > Show Implicit Hints option for the editor to display all implicit parameters and conversions.:

Conclusion

Great! You've learned how the given/using keyword works in Scala, how to use implicit parameters and conversions, and also have an understanding of how the compiler finds and substitutes values. Now, you're well-equipped to solve problems!

How did you like the theory?
Report a typo