Say goodbye to implicits

Magda Stożek

Contextual abstractions in Scala 3

Agenda

  1. About implicits
  2. Problems in Scala 2
  3. Use cases - Scala 2 vs Scala 3
  4. Migration

Implicits make us powerful

  • providing context
  • adding functionality
  • type classes
  • implicit conversions
  • dependency injection
  • expressing capabilities
  • computing new types
  • proving relationships between types

Implicits make us miserable

  • implicit parameter
  • implicit argument
  • implicit value
  • implicit object
  • implicit def
  • implicit class
  • implicit import incantations
  • sneaky conversions

Scala 3

Scala 2

  1. Providing context
  2. Adding functionality
  3. Type classes
  4. Conversions
  5. Imports
  6. Unused names
  7. Compiler errors

vs

Scala 3

1. Providing context

- implicit vals

implicit val ec: ExecutionContext = ExecutionContext.global
Future {
  def apply[T](body: => T)(implicit executor: ExecutionContext): Future[T] = {...}
}

Scala 2

val myFuture: Future[Int] = Future {
  println("Finding the answer...")
  42
}

?

?

1. Providing context

- using/given

val myFuture: Future[Int] = Future {
  println("Finding the answer...")
  42
}

Scala 3

given ec: ExecutionContext = ExecutionContext.global
Future {
  def apply[T](body: => T)(using executor: ExecutionContext): Future[T] = {...}
}

?

?

2. Adding functionality

object other_package {
  case class Book(title: String, pages: Int)
}
val book = Book("Winnie-the-Pooh", 160)
myBook.qualifiesForChallenge   //true

val book = Book("Too short", 5)
myBook.qualifiesForChallenge   //false
implicit class Qualifier(b: Book) {
  def qualifiesForChallenge: Boolean = b.pages > 20
}

?

- implicit classes

Scala 2

2. Adding functionality

- extension methods

object other_package {
  case class Book(title: String, pages: Int)
}
val book = Book("Winnie-the-Pooh", 160)
myBook.qualifiesForChallenge   //true

val book = Book("Too short", 5)
myBook.qualifiesForChallenge   //false

?

extension (b: Book)
  def qualifiesForChallenge = b.pages > 20

Scala 3

3. Type classes

  trait Scorable[T] {
    def score(t: T): Int
  }
val book = Book("Dune", 600)
val initialScore = CurrentScore(0).addItem(book)    // CurrentScore(600)

val article = Article("Understanding type classes")
val newScore = initialScore.addItem(article)        // CurrentScore(601)

?

- implicit val/object/def (with context bounds and implicitly)

  implicit val scorableBook = new Scorable[Book] {
    override def score(b: Book): Int = b.pages
  }

  implicit val scorableArticle = new Scorable[Article] {
    override def score(t: Article): Int = 1
  }

  case class CurrentScore(value: Int) {
    def addItem[T: Scorable](item: T): CurrentScore =
      CurrentScore(value + implicitly[Scorable[T]].score(item))
  }

Scala 2

?

3. Type classes

  trait Scorable[T] {
    def score(t: T): Int
  }
val book = Book("Dune", 600)
val initialScore = CurrentScore(0).addItem(book)    // CurrentScore(600)

val article = Article("Understanding type classes")
val newScore = initialScore.addItem(article)        // CurrentScore(601)

- implicit val/object/def (with implicit parameters)

  implicit val scorableBook = new Scorable[Book] {
    override def score(b: Book): Int = b.pages
  }

  implicit val scorableArticle = new Scorable[Article] {
    override def score(t: Article): Int = 1
  }

  case class CurrentScore(value: Int) {
    def addItem[T](item: T)(implicit scoring: Scorable[T]): CurrentScore =
      CurrentScore(value + scoring.score(item))
  }

Scala 2

3. Type classes

- given instances (with context bounds and summon)

?

  given Scorable[Book] with {
    override def score(b: Book): Int = b.pages
  }

  given Scorable[Article] with {
    override def score(t: Article): Int = 1
  }

  case class CurrentScore(value: Int) {
    def addItem[T: Scorable](item: T): CurrentScore =
      CurrentScore(value + summon[Scorable[T]].score(item))
  }
  trait Scorable[T] {
    def score(t: T): Int
  }

Scala 3

val book = Book("Dune", 600)
val initialScore = CurrentScore(0).addItem(book)    // CurrentScore(600)

val article = Article("Understanding type classes")
val newScore = initialScore.addItem(article)        // CurrentScore(601)

3. Type classes

- given instances (with using)

  given Scorable[Book] with {
    override def score(b: Book): Int = b.pages
  }

  given Scorable[Article] with {
    override def score(t: Article): Int = 1
  }

  case class CurrentScore(value: Int) {
    def addItem[T](item: T)(using scoring: Scorable[T]): CurrentScore =
      CurrentScore(value + scoring.score(item))
  }
  trait Scorable[T] {
    def score(t: T): Int
  }
val book = Book("Dune", 600)
val initialScore = CurrentScore(0).addItem(book)    // CurrentScore(600)

val article = Article("Understanding type classes")
val newScore = initialScore.addItem(article)        // CurrentScore(601)

Scala 3

4. Conversions

- implicit def

case class Item(value: String) extends AnyVal

def print(item: Item): Unit = println(s"*** ${item.value} ***")
val book = Book("Dune", 600)

print(book)

?

implicit def bookToItem(book: Book): Item = Item(book.title)

Scala 2

4. Conversions

- Conversion class

case class Item(value: String) extends AnyVal

def print(item: Item): Unit = println(s"*** ${item.value} ***")
val book = Book("Dune", 600)

print(book)

?

given Conversion[Book, Item] with
  def apply(book: Book): Item = Item(book.title)

Scala 3

5. Imports

- importing implicits

import scala.concurrent.ExecutionContext.Implicits.*

val future = Future(42) //OK

Scala 2

5. Imports

- importing givens

import scala.concurrent.ExecutionContext.Implicits.*

val future = Future(42) //error
import scala.concurrent.ExecutionContext.Implicits.given

val future = Future(42) //OK

Scala 3

import scala.concurrent.ExecutionContext.Implicits.{given, *}

val future = Future(42) //OK

6. Unused names

trait SeqOps (...) {
  def sorted[B](implicit ord: Ordering[B]): Seq = ...
}

- mandatory names

implicit val byTitle: Ordering[Book] = (x: Book, y: Book) => x.title.compareTo(y.title)
val myBooks = Seq(Book("Dune", 600), Book("Winnie-the-Pooh", 160))

myBooks.sorted

?

Scala 2

6. Unused names

trait SeqOps (...) {
  def sorted[B](using ord: Ordering[B]): Seq = ...
}

- anonymous givens

given byTitle: Ordering[Book] = (x: Book, y: Book) => x.title.compareTo(y.title)
val myBooks = Seq(Book("Dune", 600), Book("Winnie-the-Pooh", 160))

myBooks.sorted
given Ordering[Book] = (x: Book, y: Book) => x.title.compareTo(y.title)

?

?

Scala 3

7. Compiler errors

could not find implicit value for evidence parameter of type 
org.example.TypeClassesFullContextBoundsExample.Scorable[org.example.Domain.Book]

Scala 2

7. Compiler errors

The following import might fix the problem:

import concurrent.ExecutionContext.Implicits.global
no implicit argument of type 
org.example.TypeClassesFullExample.Scorable[org.example.domain.Book] was found 
for parameter scoring of method addItem in class CurrentScore

Scala 3

Migration

Grace period - at least 3.0 and 3.1

Summary

  • intention, not mechanism
  • given/using
  • extension methods
  • Conversion class

More


Thank you

given Finished[Presentation] with {
  override def ask(q: Question): Unit = q.ask()
}