NashTech Blog

Contextual Abstraction in Scala 3: Given and Using

Table of Contents
kaufmann, businessman, gears-3821436.jpg

One of the most powerful and widely used feature of Scala is its implicit functionality. They are the fundamental way to abstract over context. They offer help in a wide variety of use cases, including implementing type classes, context establishment, dependency injection, and many more. But with great power comes great responsibility. As powerful as implicit may be, it still comes with a set of complaints regarding its complexities. These complaints pushed the Scala Compiler team to redesign implicit in Scala 3, making it more clear and easier to use.

Caveats with Implicits

  1. Implicits are very convenient to use, hence they are easily over- and mis-used. Many newcomers start with defining implicit conversions, and they end up using it for everything.
  2. As programmers become over-reliant on implicits, a simple fix to any kind of type-error becomes an incantation of the right imports. More often than not, these imports bring a bunch of more problems.
  3. Over-using implicits, though improves the code design, but it brings a negative impact on code readability. They can be less visible than typical parameters and debugging (or reviewing) the code becomes problematic.

None of these shortcomings is fatal, but they do make code using implicits a lot more cumbersome and less clear than it could be. As a result, in Scala 3, one of the major focus was to redesign the existing implicit functionality.

Given and Using

 

Scala 3: The New Design

As a part of the redesign, Scala 3 introduces four fundamental changes:

  1. Given Instances are the new way to define an instance of implicit value. The idea is that, rather than mixing the implicit modifier with almost everything, we’ll have a single way to define values that should be implicit.
  2. Using Clauses are the new syntax for implicit parameters and their arguments.
  3. “Given” imports are a new class of import selectors that import Givens only.
  4. Implicit Conversions are now expressed as given instances of a standard Conversion class.

The New Keywords

The “given” Instance

The given keyword is used to define an instance of implicit value. Unlike implicits, we do need to import given instances explicitly in our programs. This definitely helps in tracking down the imports and debugging. Declaring a given is as simple as:

given timeout: Int = 10

It can even be anonymous like:

given Int = 10

In case it is anonymous, Scala compiler synthesizes a readable and reasonably concise name from the implemented type(s).

The “using” clause

In the functional programming paradigm, we create stateless and pure functions that often take many parameters. Some of these parameters can be highly repetitive and passed over to many functions. We can use contextual parameters to define such parameters, and the programmer does not have to write them explicitly.

In Scala 3, this is provided through the using clause. The compiler looks for parameters marked with the using clause and replaces them with a given instance of a compatible type available within the scope.

For instance, in

def getImpl(url:String)(using timeout: Int):String = "{}"

we use the using clause with timeout value marking it as an implicit value. That being said, we can still explicitly pass a value to the timeout field as:

getImpl("http://www.example.com")(using 4)

The “summon” keyword

In Scala 2, we use implicitly to get an available implicit value from the scope. For example, to get an execution context from the scope, we can write:

val ctx = implicitly[ExecutionContext]

In Scala 3, this method is removed and is replaced with summon:

val ctx = summon[ExecutionContext]

Translation from Scala 2 to Scala 3

There are a bunch of use cases where we regularly use implicits and these use case can be easily translated to work with given and using in Scala 3.

Implicit Conversion

Let’s consider the following set of code where we are handling processing time data.

case class SecondWrapper(value: Int)
object TimeUtil {
      def doSomethingWithProcessingTime(sec: SecondWrapper): String = {
        s"${sec.value} seconds"
     }
}

If we have the value for parameter sec in seconds, we want to wrap it up in the defined case class SecondWrapper.

Using Scala 2 Implicits

In Scala 2, the straightforward approach is to define an implicit method that wraps the parameter value to SecondWrapper case class and bring the implicit conversion to scope.

object ImplicitConversion {
  implicit def intToSecond(value: Int): SecondWrapper = SecondWrapper(value)
}

import ImplicitConversions._
val processingTime = 100
//auto conversion from Int to Second using intToSecond() from scope
TimeUtil.doSomethingWithProcessingTime(processingTime)
Using Scala 3

The implicit conversion, though simple, can lead to unintended errors in large projects. We can avoid this in Scala 3 using given keyword and using clause.

Using the given keyword, we can define an implicit value as:

object ImplicitConversion {
  given Conversion[Int, SecondWrapper] = SecondWrapper(_)
 }

Now by importing the above instance in the right place, we will be able to implicitly implement our conversion:

object Usage {
  //We need to use ImplicitConversion.given to bring the conversion into scope.
  //Even with * to import, it will not bring the given instances to scope.
  import ImplicitConversion.given
  val processingTime = 100
  //auto-conversion from Int to Second using given.
  TimeUtil.doSomethingWithProcessingTime(processingTime)
  }

Contextual Parameters

Let’s consider a method that combines a prefix to a passed string. Assuming that the prefix is static in nature, instead of passing the same prefix value in all the methods repeatedly, we can define it as an implicit in Scala 2 (or given in Scala 3) and use it wherever required.

Using Scala 2
class Prefixer(val prefix: String)
def addPrefix(s: String)(implicit p: Prefixer) = p.prefix + s
//We can create an implicit for our static prefix
implicit val myImplicitPrefixer = new Prefixer("***")
//As long as it is in scope, we can automatically pass the given
addPrefix("abc") //returns ***abc

//If we want to explicitly pass the prefix
addPrefix("abc")(p = new Prefixer("+++")) //returns +++abc
Using Scala 3

The same can be implemented in Scala 3 using givens and using clause as:

class Prefixer(val prefix: String)
def addPrefix(s: String)(using p: Prefixer) = p.prefix + s
//We can create a given for our static prefix
given myImplicitPrefixer = new Prefixer("***")
//As long as it is in scope, we can automatically pass the given
addPrefix("abc") //returns ***abc

//If we want to explicitly pass the prefix
addPrefix("abc")(using new Prefixer("+++")) //returns +++abc

Conclusion

Overall, the new abstraction design in Scala 3 achieves a better separation of term inference from the rest of the language:

  • There is a single way to define givens instead of a multitude of forms all taking an implicit modifier.
  • There is a single way to introduce implicit parameters and arguments instead of conflating implicit with normal arguments.
  • There is a separate way to import givens that does not allow them to hide in a sea of normal imports.
  • And there is a single way to define an implicit conversion which is clearly marked as such and does not require special syntax.

This design thus avoids feature interactions and makes the language more consistent and orthogonal. It will make implicits easier to learn and harder to abuse.

References

  1. Context Parameters
  2. Contextual Abstractions
  3. Using and Given in Scala 3
Picture of Swantika Gupta

Swantika Gupta

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top