Baeldung Pro – Scala – NPI EA (cat = Baeldung on Scala)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

1. Introduction

In Scala 3, context functions are functions with only implicit parameters or, more precisely, context parameters. This means the compiler will input the correct arguments (taking them from the “context”, aka the scope) rather than explicitly writing them in the code.

In this tutorial, we’ll examine context functions and how to use them.

2. The Syntax

In Scala 3, we write the type of a context function using ?=>. This is different from “ordinary” functions, where we use the “arrow” sign: =>.

As we saw above, context functions do not have any explicit parameters. Therefore, to use one of the arguments in the implementation, we’ll have to use summon[T], which is similar to implicitly[T] in Scala 2. In brief, summon[T] looks for an implicit value of type T in the scope. In Scala 3, we can provide such values using given.

Let’s see an example of a context function to increment a number:

val increment: Int ?=> Int = summon[Int] + 1

We define a context function simply by specifying its type and body, just as we’d do with a “regular” function. The Int ?=> Int notation is a shortcut for ContextFunction1[Int, Int], as much as Int => Int is syntactic sugar for Function1[Int, Int].

For context functions, however, the type is mandatory. If we omitted Int ?=> Int, the compilation would fail:

No given instance of type Int was found for parameter x of method summon in object Predef
val increment = summon[Int] + 1

To invoke our function, we have to provide an implicit parameter either explicitly or via the implicit scope:

// Implicit parameter provided via the scope
given Int = 1
println(s"Result scope: $increment")

// Implicit parameter provided explicitly to the call
println(s"Result explicit: ${increment(using 5)}")

In the first case, we used given to put an Int into the implicit scope. Here, we can invoke increment simply by calling it. In the second case, we explicitly provided the parameter with using at the call site. If we run the example above, we’ll see two lines in the output:

Result scope: 2
Result explicit: 6

2.1. Multiple Context Parameters

Context functions can have more than a single parameter. Let’s see an example:

val repeatString: String ?=> Int ?=> String = summon[String].repeat(summon[Int])

println(s"Result repeatString: ${repeatString(using "Baeldung")(using 3)}")

In the example above, we defined a function with two context parameters, a String and an Int, and invoked it just as before:

Result repeatString: BaeldungBaeldungBaeldung

3. An Example: Modeling Kotlin Scope Functions

Let’s now see a more complex example of a context function. We’ll use them to model one of Kotlin’s scope functions, also.

In Kotlin, also is a function taking the context object as a parameter, with name it, and returning the context object itself. In brief, whenever we see also in the code we can think of a function that uses a given object and returns it, without modifying it.

Let’s take a look at Scala’s implementation:

opaque type WrappedAlso[T] = T

def it[T](using a: WrappedAlso[T]): T = a

extension [T](x: T)
  def also(f: WrappedAlso[T] ?=> Unit): T =
    f(using x)
    x

First, we modeled an opaque type, named WrappedAlso, which essentially wraps a value of type T. This is always advised with context functions to prevent unwanted givens from being picked from the scope.

Secondly, we implemented a constructor to avoid explicitly summoning a parameter of type T. To comply with Kotlin’s naming convention, we call this method it.

Lastly, we defined an extension method, called also, doing all the magic. It inputs a context function type WrappedAlso[T] ?=> Unit, which means we’ll have to provide a context function using a value of type T. also invokes the function f with x as an explicit parameter, discards its result, and returns the original value x.

Since WrappedAlso is an opaque type alias, its values won’t need boxing. Furthermore, since also is added as an extension method, its argument does not need boxing either.

Let’s see how we can use our new function:

val numbers = List(1, 2, 3, 4, 5)
numbers
  .also(println(s"The list before adding 6: $it"))
  .appended(6)
  .also(println(s"The list after adding 6: $it"))

In the example above, we defined a list of numbers and made two calls to also, before and after adding an element. In the function body, we can refer to the context parameter with it, as much as we’d do in Kotlin.

If we hadn’t defined the it constructor, we would have had to refer to it with an explicit summon[List[Int]], which is far less readable.

Running the example produces the expected output:

The list before adding 6: List(1, 2, 3, 4, 5)
The list after adding 6: List(1, 2, 3, 4, 5, 6)

Lastly, any changes to the list in the body of the also function will not get propagated:

numbers
    .also(it appended 7)
    .also(println(s"The list after adding 7: $it"))

The example above produces The list after adding 7: List(1, 2, 3, 4, 5) as an output.

4. Conclusion

In this article, we learned about context functions are in Scala 3. We started with an overview of the syntax, and then we deepened our knowledge with functions with more than one parameter. Finally, we looked at a practical example, showing how simple it is to model idiomatic Kotlin scope functions.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.