An Introduction to Givens in Scala 3

Scala 3.0
Knoldus Blog Audio
Reading Time: 3 minutes

In this blog, I am going to discuss about a new and exciting feature which is being introduced in Scala 3 – givens. In my previous blog posts, I have written about some other new Scala 3 features. You can check them out here.

A little bit of the background first. Scala 2 has a very powerful feature called implicits. While implicits have been extremely useful, they are also overused and misused. And a long and detailed list of some of the criticism has been shared by the creators here. Therefore, the creators of Scala have done a great amount of work to improve the language features to retain the concept while removing the pitfalls.

What are givens in Scala 3?

Givens are a way to ensure that the compiler helps us to fill in the needed contextual information without us explicitly passing it. Let us take an example here. For instance, we have created an application which helps to calculate the salary of an employee. The salary is the total of the basic salary and the bonus given by the company. Here the bonus can be treated as contextual information. The using keyword is used to represent context parameters which will be supplied by the compiler.

object CompanySalaryApplication:
case class Employee(name: String, salary: Int)
case class CompanyBonus(amount: Int)
trait SalaryComputation:
def computeSalary(employee: Employee)(using companyBonus: CompanyBonus): Int =
employee.salary + companyBonus.amount

Now, at the caller site, we mark our context parameter as given so that it can be supplied by the compiler. Therefore, we will not need to explicitly pass it.

import CompanySalaryApplication._
object CompanyABonus:
given CompanyBonus = CompanyBonus(1000)
object CompanyA extends App with SalaryComputation:
import CompanyABonus.given
val jack = Employee("Jack", 8000)
val jill = Employee("Jill", 10000)
println(s"Jack's salary is ${computeSalary(jack)}")
println(s"Jill's salary is ${computeSalary(jill)}")
view raw CompanyA.scala hosted with ❤ by GitHub
import CompanySalaryApplication._
object CompanyBBonus:
given companyBonus: CompanyBonus = CompanyBonus(2000)
object CompanyB extends App with SalaryComputation:
import CompanyBBonus.given
val julie = Employee("Julie", 8000)
val julia = Employee("Julia", 10000)
val employees = List(julie, julia)
val employeeAndSalary = employees.map(emp => emp.name > computeSalary(emp))
println(s"Map of employee and salary for company B is $employeeAndSalary")
view raw CompanyB.scala hosted with ❤ by GitHub

As can be noticed, that we have used two styles to compute the salary. CompanyA computes it individually for its employees whereas CompanyB computes it using a List. At both the sites, we have not had to pass the givens explicitly and the compiler takes care of it.

A important thing to note here is that givens cannot be passed as regular parameters are. That is, doing the following is a compilation error.

val employeeAndSalary = employees.map(emp => emp.name -> computeSalary(emp)(companyBonus))

If we want to pass the givens as parameters, then we need to use the using keyword. This would work.

val employeeAndSalary = employees.map(emp => emp.name -> computeSalary(emp)(using companyBonus))

Anonymous Givens

If we take a closer look at CompanyABonus object’s given and CompanyBBonus object’s given, you will notice that CompanyBBonus given has a name, companyBonus, where CompanyABonus given does not. This is because Scala allows anonymous givens. What is declared in CompanyABonus object is an anonymous given. This is possible because of term inference where we give the type and the compiler synthesizes a term for us. This is different from type inference, where we give the term and compiler deciphers the type automatically.

Importing Givens

It is recommended by the creators of Scala 3, that the logical home for the givens should be the companion object of the class. However, this will not always be the case. Therefore, the second logical home for them is recommended is to be a singleton object that will not be automatically found. In such a case, we will need to import them, as we have also done in the example above. Suppose CompanyA object contained more methods apart from the given.

object CompanyABonus:
given CompanyBonus = CompanyBonus(1000)
def otherDetails: List[String] = ???

The way to import everything in Scala is to use the wildcard(*) symbol. However, in Scala 3, there is a limitation to this. The wildcard will import everything but givens. They have to be explicitly imported. And one way is to use the given wildcard, as we have done in the example above. This imports all the givens. If we do not want to import all, then we can import using the name.

import CompanyBBonus.companyBonus

We can also import givens by their type. The below would work just fine for CompanyB.

import CompanyBBonus.{given CompanyBonus}

Limiting the import of givens and having to use more syntax to import them seems to be a conscious and the right choice. As, one of the problems in Scala with implicits was to figure out from where the implicit is getting imported. A developer may need to visit many files before figuring where the implicit is present. Therefore, with how givens are imported, this problem will be solved as we will be easily able to decipher where given resides from the syntax.

This is all that I wanted to talk about in this blog post. However, in the upcoming posts, I will want to dive more into this new feature of Scala 3. Stay tuned.

References:

  1. https://dotty.epfl.ch/docs/Contextual%20Abstractions/index.html
  2. https://www.artima.com/shop/programming_in_scala_5ed
Knoldus-blog-footer-image

Written by 

Sonu Mehrotra is a Software Developer with industry experience of 5+ years. He is a Clean Code practitioner and an Agile follower. Sonu has helped financial institutions develop trading systems that make trading activities easier and more accessible to customers.