This is an excerpt from the Scala Cookbook, 2nd Edition. This is Recipe 23.8, Using Term Inference with Given and Using.
Scala 3 Problem
Using Scala 3, you have a value that’s passed into a series of function calls, such as using an ExecutionContext when you’re working with Scala Futures or Akka actors:
doX(a, executionContext)
doY(b, c, executionContext)
doZ(d, executionContext)
Because this type of code is repetitive and makes the code harder to read, you’d prefer to write it like this instead:
doX(a)
doY(b, c)
doZ(d)
You may not know it yet, but this means that you want to know how to use Scala 3 term inference, what used to be known as implicits in Scala 2.
Scala 3 Solution
This solution involves multiple steps:
-
Define your “
giveninstances” using the Scala 3givenkeyword-
This typically involves the use of a base trait and multiple “givens” that implement that trait
-
-
When declaring the “implicit” parameter your function will use, put it in a separate parameter group and define it with the
usingkeyword -
Make sure your
givenvalue is in the current context when your function is called
In the following example I’ll demonstrate the use of an Adder trait and two given values that implement the Adder trait’s add method.
Step 1: Define your “given instances”
In the first step you’ll typically create a parameterized trait like this:
trait Adder[T]:
def add(a: T, b: T): T
Then you’ll implement the trait using one or more given instances, which you define like this:
given intAdder: Adder[Int] with
def add(a: Int, b: Int): Int = a + b
given stringAdder: Adder[String] with
def add(a: String, b: String): String = s"${a.toInt + b.toInt}"
In this example, intAdder is an instance of Adder[Int], and defines an add method that works with Int values. Similarly, stringAdder is an instance of Adder[String] and provides an add method that takes two strings, converts them to Int values, adds them together, and returns the sum as a String. (To keep things simple I don’t account for errors in this code.)
If you’re familiar with creating implicits in Scala 2, this new approach is similar to that process. The idea is the same, it’s just that the syntax has changed.
Step 2: Declare the parameter your function will use with the “using” keyword
Next, declare your functions that use the Adder instances. When doing this, specify the Adder parameter with the using keyword. Put the parameter in a separate parameter group, as shown here:
def genericAdder[T](x: T, y: T)(using adder: Adder[T]): T =
adder.add(x, y)
The keys here are that the adder parameter is defined with the using keyword in that separate parameter group:
def genericAdder[A](x: A, y: A)(using adder: Adder[A]): A =
-----------------------
Also notice that genericAdder declares the generic type A. This function doesn’t know if it will be used to add two integers or two strings; it just calls the add method of the adder parameter.
Tip: In Scala 2, parameters like this were declared using the
implicitkeyword, but now, as the entire programming industry has various implementations of this concept, this parameter is now known as a context parameter, and it’s declared with the Scala 3usingkeyword as shown.
Step 3: Make sure everything is in the current context
Finally, assuming that intAdder, stringAdder, and genericAdder are all in scope, your code can call the genericAdder function with Int and String values, without having to pass instances of intAdder and stringAdder into genericAdder:
println(genericAdder(1, 1)) // 2
println(genericAdder("2", "2")) // 4
The Scala compiler is smart enough to know that intAdder should be used in the first instance, and stringAdder should be used in the second instance. This is because the first example uses two Int parameters and the second example uses two String values.
If you need to import your givens into the current scope, see the “Importing Givens” section below.
More: Anonymous givens and unnamed parameters
There’s often no reason to give a given a name, so you can also use this “anonymous given” syntax instead of the previous syntax:
given Adder[Int] with
def add(a: Int, b: Int): Int = a + b
given Adder[String] with
def add(a: String, b: String): String = "" + (a.toInt + b.toInt)
If you don’t reference a context parameter inside your method it also doesn’t require a name, so if genericAdder didn’t reference the adder parameter, this line:
def genericAdder[A](x: A, y: A)(using adder: Adder[A]): A = ...
could be changed to this:
def genericAdder[A](x: A, y: A)(using Adder[A]): A = ...
Discussion
In the example shown in the Solution you could have passed the intAdder and stringAdder values in manually:
println(genericAdder(1, 1)(using intAdder))
println(genericAdder("2", "2")(using stringAdder))
But the point of using given values in Scala 3 is to avoid this repetitive code.
The reason for the significant syntax change in Scala 3 is that the Scala creators felt that the implicit keyword was overused in Scala 2: it could be used in several different places, with different meanings in each place.
Conversely, the new given and using syntax is more consistent and more obvious. For example, you might read the earlier code as, “Given an intAdder and a stringAdder, use those values as the adder parameter in the genericAdder method.”
Discussion: Create your own API with extension methods
You can combine this technique with extension methods to create your APIs. For example, given this trait that has two extension methods:
trait Math[T]:
def add(a: T, b: T): T
def subtract(a: T, b: T): T
// extension methods: create your own api
extension (a: T)
def + (b: T) = add(a, b)
def - (b: T) = subtract(a, b)
You can create two given instances as before:
given intMath: Math[Int] with
def add(a: Int, b: Int): Int = a + b
def subtract(a: Int, b: Int): Int = a - b
given stringMath: Math[String] with
def add(a: String, b: String): String = "" + (a.toInt + b.toInt)
def subtract(a: String, b: String): String = "" + (a.toInt - b.toInt)
Now you can create genericAdd and genericSubtract functions:
// `+` here refers to the extension method
def genericAdd[T](x: T, y: T)(using Math: Math[T]): T =
x + y
// `-` here refers to the extension method
def genericSubtract[T](x: T, y: T)(using Math: Math[T]): T =
x - y
Now you can use the genericAdd and genericSubtract functions without manually passing them the intMath and stringMath instances:
println("add ints: " + genericAdd(1, 1)) // 2
println("subtract ints: " + genericSubtract(1, 1)) // 0
println("add strings: " + genericAdd("2", "2")) // 4
println("subtract strings: " + genericSubtract("2", "2")) // 0
Once again the compiler can determine that the first two examples need the intMath instance, and the last two examples need the stringMath instance.
Discussion: Alias givens
The given documentation states, “an alias can be used to define a given instance that is equal to some expression.” To demonstrate this, imagine that you’re creating a search engine that understands different contexts. This might be a search engine like Google, or a tool like Siri and Alexa, where you want your algorithm to participate in an ongoing conversation with a human.
For example, when someone performs a series of searches, they may be interested in a particular context like “Food” or “Life”:
enum Context:
case Food, Life
Given those possible contexts, you can write a search function to look up word definitions based on the context:
import Context._
// imagine some large decision-tree that uses the Context
// to determine the meaning of the word that’s passed in
def search(s: String)(using ctx: Context): String = ctx match
case Food =>
s.toUpperCase match
case "DATE" => "like a big raisin"
case "FOIL" => "wrap food in foil before baking"
case _ => "something else"
case Life =>
s.toUpperCase match
case "DATE" => "like going out to dinner"
case "FOIL" => "argh, foiled again!"
case _ => "something else"
Now in an ongoing conversation between a human and your algorithm, if the current context is Food, you’ll have a given like this:
given foodContext: Context = Food
That syntax is known as an alias given`, and `foodContext is a given of type Context. Now when you call the search function, that context is magically pulled in:
val date = search("date")
This results in date being assigned the value, "looks like a big raisin". Note that you can still pass in the Context explicitly, if you prefer:
val date = search("date")(using Food) // "looks like a big raisin"
val date = search("date")(using Life) // "like going out to dinner"
But again, the assumption here is that a function like search may be called many times, and the desire is to avoid having to manually declare the context parameter.
Discussion: Importing givens
When a given is defined in a separate module, which it usually will be, it must be imported into scope with a special import statement. This example shows the technique:
object Adder:
trait Adder[T]:
def add(a: T, b: T): T
given intAdder: Adder[Int] with
def add(a: Int, b: Int): Int = a + b
@main def GivenImports =
import Adder._ // import all non-given definitions
import Adder.given // import the `given` definition
def genericAdder[A](x: A, y: A)(using adder: Adder[A]): A = adder.add(x, y)
println(genericAdder(1, 1))
Per the Importing Givens documentation, there are two benefits to this new import syntax:
-
Compared to Scala 2, it’s more clear where givens in scope are coming from
-
It enables importing all givens without importing anything else
Note that the two import statements can be combined into one:
import Adder.{given, *}
It’s also possible to import given values by their type:
object Adder:
trait Adder[T]:
def add(a: T, b: T): T
given Adder[Int] with
def add(a: Int, b: Int): Int = a + b
given Adder[String] with
def add(a: String, b: String): String =
s"${a.toInt + b.toInt}"
@main def GivenImports =
// when put on separate lines, the order of the imports is important
import Adder.*
import Adder.{given Adder[Int], given Adder[String]}
def genericAdder[A](x: A, y: A)(using adder: Adder[A]): A = adder.add(x, y)
println(genericAdder(1, 1)) // 2
println(genericAdder("2", "2")) // 4
In this example the given imports can also be specified like this:
import Adder.{given Adder[?]}
or this:
import Adder.{given Adder[_]}
See the Importing Givens documentation for all up to date import usages.
| this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
See Also
-
See the Scala 3 Given Instances page for more details on givens
-
See the Scala 3 Importing Givens page for more details on importing givens
-
The Scala 3 Overview: Critique of the Status Quo document details the motivations behind changing from implicits to given instances