Scala 3: Using Term Inference with Given and Using

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 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)

Therefore, you want to know how to use Scala 3 term inference, what used to be known in Scala 2 as implicits.

Scala 3 Solution

This solution involves multiple steps:

  1. Define your “given instances” using the Scala 3 given keyword

    1. This typically involves the use of a base trait and multiple “givens” that implement that trait

  2. When declaring the “implicit” parameter your function will use, put it in a separate parameter group and define it with the using keyword

  3. 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 3 using 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.

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.”

Create your own API with extension methods

You can combine this technique with extension methods — which are demonstrated in [methods-extension-methods-intro] — 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.

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.

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. That syntax is demonstrated in [importing-givens-intro], and this example also 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

Released in Sep., 2022

See Also