Explicit term inference with Scala 3

Meriam Lachkar, Vincenzo Bazzucchi, Scala Center

Explicit term inference with Scala 3

One of the most striking changes for developers adopting Scala 3 is the introduction of a new syntax to replace the implicit mechanism used in previous Scala versions.

The motivation behind the new syntax is that the same implicit keyword was used for different purposes and patterns and thus it became a way to express how to implement patterns. This means that when encountering this ambiguous incantation, users need to decipher what the intent of the developer was: is this a conversion? Does this avoid parameter repetition? Is this an extension of a type? Is this a typeclass? How do I import this?

Seeing how pervasive implicits became in libraries and projects, Scala 3 aims at reducing confusion and cognitive load by using new keywords that convey the intent of the developer.

This post briefly introduces the new syntax and semantics available to Scala 3 programmers by analysing the most common use cases and patterns: extension methods, implicit parameters, implicit conversions and typeclasses.

Compatibility disclaimer

While we believe that the new syntax represents an improvement, it is very important to highlight that older code using implicit is still perfectly valid for the Scala 3.0 compiler, even if it will be deprecated in future releases. You do not need to port your codebase right away, it can be an incremental and gradual process.

Extension methods

When you do not have control over a type but you need to extend its capabilities with a new method, Scala 3 allows you to define an extension method.

Assume that you are working with List[Try[String]] and that you often need to retrieve the elements for which the computation succeeded.

Then you can extend this type to have a collectSucceded method:

// ListTryOps.scala

import scala.util.{Try, Success}

extension (ls: List[Try[String]]) def collectSucceeded: List[String] =
  ls.collect { case Success(x) => x }

To remember the syntax, notice how collectSucceeded follows the object on which it will be available: ls.collectSucceeded. Extension methods can have type parameters as well:

extension [A] (ls: List[Try[A]]) def collectSucceeded: List[A] =
  ls.collect { case Success(x) => x }

Finally, you can add several methods without repeating the extension declaration:

extension [A] (ls: List[Try[A]])
  def collectSucceeded: List[A] =
    ls.collect { case Success(x) => x }
  def getIndexOfFirstFailure: Option[Int] =
    ls.zipWithIndex.find((t, _) => t.isFailure)
      .map(_._2)

Extensions can be imported by name or using the _ wildcard:

// Main.scala
import scala.util.Try
// import ListTryOps._ // Using wildcard
import ListTryOps.collectSucceeded

def getLastTweet(username: String): Try[String] = ???

@main def main =
  val niceTweets = List(getLastTweet("scala_lang"), getLastTweet("odersky")).collectSucceeded
  println(niceTweets)

How was this done in Scala 2?

This pattern was particularly cumbersome to implement prior to Scala 3. A typical approach would be the following:

implicit class ListTryExtension[A](private val ls: List[Try[A]]) extends AnyVal {
  def collectSucceeded: List[A] = ls.collect { case Success(a) => a }
  def getIndexOfFirstFailure: Option[Int] =
    ls.zipWithIndex.find { case (t, _) => t.isFailure }
      .map { case (_, index) => index }
}

Note that you need to define a name for the class, even if this is probably never going to be used besides the import statement. You also need to understand what AnyVal is, why it is good practice to extend it and what its limitations are.

Some experience is required when reading this code to understand that its only goal is to add a couple of methods to List[Try[A]].

Find out more

You can find more information about extension methods on the dedicated documentation page. We also suggest that you read how they complement another new Scala 3 feature: opaque types. Later in this post we will see how they simplify a very common pattern: typeclasses.

Avoiding repetition with contextual parameters

Similarly to other programming languages, Scala allows you to omit the type of a variable as the compiler can perform type inference. For example we can write

val x = 3
val y = x + 1

instead of

val x: Int = 3
val y: Int = x + 1

In this case we declared the value and the compiler inferred the corresponding type. One of the distinctive features of Scala is being able to infer values when the type is specified.

Consider the Future abstraction in the standard library. Each time you create a Future by providing a computation, you need to specify on which ExecutionContext the computation will be evaluated:

import scala.concurrent._

def factorial(n: Int): Int = ???
def fibonacci(n: Int): Int = ???

@main def main =
  val executor: ExecutionContext = ExecutionContext.global

  val fact100 = Future(factorial(100))(executor)
  val fibo100 = Future(fibonacci(100))(executor)
  // ...

As you can see, the repetition of executor quickly becomes a tedious task. We can declare that this parameter is common to the current context and avoid its repetition:

@main def main =
  given executor as ExecutionContext = ExecutionContext.global
  val fact100 = Future(factorial(100))
  val fibo100 = Future(fibonacci(100))

This works because the apply method of future could be declared as follows:

// scala.concurrent.Future.scala
object Future {
    // ..
    def apply[T](body: => T)(using executor: ExecutionContext): Future[T] = // ...
}

We see here the other half of the syntax: if we declare a parameter as using, the compiler will search the current scope at call sites for given values with a compatible type.

Note that the executor identifier is never used, so we can omit it:

given ExecutionContext = ExecutionContext.global

the same is possible for the using parameter:

def apply[T](body: => T)(using ExecutionContext): Future[T] = // ...

Note that in Scala 3 the type of a given definition must be explicit.

Another important aspect is how we import these values. For example, if you had a more involved definition for the ExecutionContext that is used in multiple files, you could refactor it into a different file:

// Context.scala
import scala.concurrent.ExecutionContext
import java.util.concurrent.Executors

object Context:
  given ExecutionContext =
    ExecutionContext.fromExecutor(Executors.newFixedThreadPool(5))

Then you can import it using a wildcard:

import Context.given

Or by making the type that you are bringing in scope explicit:

import Context.{given ExecutionContext}

This allows you to have more control over imports without relying on instance names.

Find out more

You can learn more about using / given and about the rules of resolution in the documentation.

How was this done in Scala 2?

In Scala 2 this pattern was achieved by marking both the value and the parameter with the implicit keyword. The previous example would look like this:

// Main.scala
import scala.concurrent._
object Main extends App
  implicit val ec: ExecutionContext = ExecutionContext.global
  Future(println("Hello World"))
}

// scala.concurrent.Future.scala
object Future {
    // ..
    def apply[T](body: => T)(implicit executor: ExecutionContext): Future[T] = // ...
}

We note again that we had to provide a name which might not be used for the variable. The implicitly function from Scala 2 is renamed to summon in Scala 3.

Finally the special import syntax allows users to explicitly import given instances by type rather than by name. This makes more sense since we usually refer to them by type as well.

The context bound syntax remains unchanged.

Automatically converting values between types

The implicit conversion feature is dangerous. For this reason, in Scala 3, the compiler will warn you every time it is used. You can disable the warning at your own risk by explicitly importing the feature into the current scope.

The Java standard library provides an Optional type which is very similar to Option. If you are working with a Java library which produces a lot of objects with this type, but you also have many Options around, you might want to define an automatic conversion.

This is done by extending a new trait defined in the standard library:

abstract class Conversion[-T, +U] extends (T => U)

In Scala 3 you can define it in this way:

// OptionalConversion.scala
import java.util.Optional

object OptionalConversion:

  given [A]: Conversion[Optional[A], Option[A]] with
    def apply(in: Optional[A]): Option[A] =
      if in.isPresent then Some(in.get())
      else None

You can then use the syntax described above for the import:

import java.util.Optional
import OptionalConversion.given

@main def main =
  val opt: Option[Int] = Optional.empty[Int]()

As mentioned above, the compiler will warn you about this dangerous feature:

Use of implicit conversion class given_Conversion_Optional_Option in object OptionalConversion should be enabled
by adding the import clause 'import scala.language.implicitConversions'
or by setting the compiler option -language:implicitConversions.
See the Scala docs for value scala.language.implicitConversions for a discussion
why the feature should be explicitly enabled.

The warning can be silenced by adding import scala.language.implicitConversions, as the message suggests.

Find out more

You can learn more about this feature in the documentation.

How was this done in Scala 2?

Scala 2 relied on implicit defs and implicit function values to implement this pattern:

import java.util.Optional

object OptionalConversion {
  implicit def optionalToOption[A](in: Optional[A]): Option[A] =
    if (in.isPresent) Some(in.get())
    else None
}

which could then be imported:

import OptionalConversion._

Note that at a glance, it is not clear that the definition is intended as an automatic conversion to an expected type:

  • the name of the method can be a hint, but this is merely a convention that other developers might not share
  • the implicit def may be intended give you extension methods on its parameter type, depending on its result
  • there is not straightforward way to verify that this function will not be used as implicit argument for a function with an implicit parameter list

The Conversion trait makes the definition explicit and more readable.

A common pattern revisited in Scala 3: Typeclasses

This pattern is fundamental to Scala functional programming libraries such as Cats. In Scala 2 we used to heavily rely on implicit conversions to add methods and on implicit parameters to propagate instances, which was a bit cryptic to beginners who were maybe already struggling with new concepts in functional programming.

In Scala 3, this pattern becomes simpler thanks to the new syntax. Let’s consider a simple typeclass such as Show which describes the capability of types to have a String representation.

We first describe the interface that all instances of this typeclass should implement:

// Show.scala
trait Show[A]:
  extension (a: A) def show: String

We can then add a companion object to provide a couple of auxiliary methods and instances that make instance creation less tedious:

// Show.scala
object Show:
  def from[A](f: A => String): Show[A] = new Show[A]:
    extension (a: A) def show: String = f(a)

  given[A: Show]: Show[List[A]] with
    extension (ls: List[A]) def show: String = ls.map(_.show).mkString(", ")

To use it we need to import the interface and define an instance:

// Main.scala
import Show.{_, given}

case class Mountain(name: String, height: Int)

given Show[Mountain] =
  Show.from((m: Mountain) => s"${m.name} is ${m.height} meters high")

@main def main =
  val mountains = List(Mountain("Mont Blanc", 4808), Mountain("Matterhorn", 4478))
  println(mountains.show)

As the extension method is defined inside the trait, there is no additional import statement required contrarily to what used to happen in Scala 2, as you will see in the following section.

How was this done in Scala 2?

In Scala 2 there was more boilerplate code involved. It all starts with defining the interface:

// Show.scala
trait Show[A] {
  def show(a: A): String
}

The second step was to define the extension method which relies on two implicits:

// ShowOps.scala
object ShowOps {
  implicit class showOps[A](in: A) extends AnyVal {
    def show(implicit instance: Show[A]): String =
      instance.show(in)
  }
}

And finally we can define the auxiliary methods in the companion object:

// Show.scala
object Show {
  def from[A](f: A => String): Show[A] = new Show[A] {
    def show(a: A): String = f(a)
  }

  // Either this or import ShowOps and use context bound
  implicit def showList[A](ls: List[A])(implicit instance: Show[A]) =
    ls.map(instance.show).mkList(",")
}

which brings us to the main definition:

// Main.scala
import Show._
import ShowOps._

case class Mountain(name: String, height: Int)

object Main extends App {
  implicit val mountainShow: Show[Mountain] =
    Show.fromFunction((m: Mountain) => s"The ${m.name} is ${m.height} meters high")

  val mountains = List(Mountain("Mont Blanc", 4808), Mountain("Matterhon", 4478))
  println(mountains.show)
}

We believe that this example shows how implicit was used to achieve different goals and, in doing so, used to be more confusing:

  • if you need to add methods, use extension rather than an implicit class
  • If you need an implicit parameters, use using to declare what you will need
  • If you are providing an implicit instance, use given to declare that the value is now available
  • If you need an implicit conversion, make it explicit for future readers
  • If you are never going to refer to a value by its name, do not provide one

Find out more

Read more about typeclass implementation in Scala 3 in the documentation.

Another extremely interesting, yet more advanced, feature in Scala 3 related to contextual abstractions are Context Functions.

Conclusion

We went over the main use cases of implicit in Scala 2 and offered a quick glance of what they would look like in Scala 3. While the final result is almost the same, code is more explicit and readable so that you can focus on solving your business problems rather than on syntax.

This is part of a larger set of usability and ergonomy improvements for Scala 3 that we believe will make the language easier and more fun to use and we are very excited to see what the community will create with them.