1. Overview

In this short tutorial, we’ll first learn how we implemented contextual abstractions until Scala 2.

Then we’ll learn about the new features introduced in Scala 3, specifically the given instance and the using clause, and see how they provide a much safer option using the help of some simple examples.

2. Brief History of Implicits

Up until Scala2, implicits were the only mechanism for obtaining contextual abstractions. Implicits are a very powerful feature, but their power is misused in many applications and has caused several problems that were difficult to debug and rectify later on. To address those issues, Scala 3 introduced many redesigns, which gave birth to the new features, given instances and the using clause.

More detailed analyses and explanations of the redesigns are available in our earlier article, Scala 3 Implicit Redesign.

3. The given Instance

A given instance is the mechanism to pass contextual information automatically by the compiler. Unlike implicit, we need to import given instances (or simply givens, for short) explicitly in our programs. This helps us in tracking down the imports and helps in debugging.

Let’s assume we’re developing a shopping cart for an e-commerce application, and we have a requirement to display items in sorted order. So, let’s first define the Item case class:

case class Item(name:String, price:Double)

Now we need to define an ordering for our Item class. Since we don’t want to pass the ordering parameter explicitly in every function call, we’re going to create it as a contextual parameter by defining a given instance:

given itemOrdering:Ordering[Item] = new Ordering[Item]{
  override def compare(i1: Item, i2: Item): Int = i1.price.compareTo(i2.price)
}

Here we defined a named instance, itemOrdering. However, it’s not mandatory to name a given in all circumstances. Those without names are known as the anonymous givens. Let’s declare a new given instance for a dummy Item object without giving it a name:

given Item = Item("Dummy", 0.0)

Note that we haven’t given a name for the instance. In this case, the compiler will automatically formulate a name for it.

4. The using Clause

In the functional programming paradigm, we create stateless and pure functions that often take many parameters. Some of these parameters are highly repetitive and passed over to many functions.

We can use contextual parameters to define such parameters. In Scala 3, this feature 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.

Let’s create our shopping cart by creating a list of Items:

val shoppingCart = List(
  Item("PanCake", 4),
  Item("Coke", 1),
  Item("Pizza", 5),
  Item("Burger", 3)
)

Now, we need a function to list items in sorted order and with a page limit:

def listItems(products: Seq[Item])(using ordering: Ordering[Item])(using limit:Int) = {
    products.sorted.take(limit)
}

We’ve declared the ordering parameter and the page limit as contextual arguments via the using clause. The compiler will pick the right parameters from the contextual variables and pass them on to the function call.

This helps us in reducing redundancy and writing more clean and concise code. In modular-designed applications, the given instances and their usages would be in different packages. In such cases, we’ll have to import the givens from another package.

Let’s import our given parameters from an object named Givens and try to invoke the listing function and see the results:

import Givens.{priceOrdering, pageLimit}

Now, we may have a question in our minds: What if there are multiple givens of the same type imported within the scope? In that case, the compiler will throw an error: ambiguous implicit arguments.

Now everything is in place and we’re ready to call our listing function without explicitly passing ordering and pageLimit. Let’s observe the output:

val sortedItems = listItems(shoppingCart)
println(sortedItems) // prints -> List(Item(Coke,1.0), Item(Burger,3.0))

As we can see, the ordering and pageLimit parameters are taken from the program context.

5. Conclusion

In this short tutorial, we’ve learned the new features (given and using) introduced in Scala 3 for implementing contextual abstractions. Then we saw how they try to solve the problems induced by the implicits. We also learned how to import givens declared in other packages and how they behave in case of conflicts.

As usual, the full source code can be found over on GitHub.


» 下一篇: Ammonite脚本