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 “
given
instances” using the Scala 3given
keyword-
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
using
keyword -
Make sure your
given
value 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
implicit
keyword, 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 3using
keyword 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