Scala Matsuri 2023 一日目の発表 Say goodbye to implicits (https://slides.com/magdastozek/goodbye-implicits-20) を受けて、Scala 2 の implicits を Scala 3 の機能と比較してみる.

implicits の機能

  • 文脈の引き回し

  • 既存の型への機能の追加 (Enrich my library)

  • 型クラス

  • 暗黙型変換 (implicit conversion)

  • 依存性の注入 (dependency injection)

  • expressing capabilities

  • computing new types

  • proving relationships between types

Scala 2 における implicits 関連の機能

  • implicit parameter (文脈の引き回し・型クラスインスタンスの適用)

  • implicit argument

  • implicit value (型クラスインスタンスの定義)

  • implicit object

  • implicit def (暗黙の型変換・(型コンストラクタのための) 型クラスインスタンスの定義)

  • implicit class (Enrich my library パターン・型クラス syntax の導入)

  • implicit import incantations (呪術的な import 文)

  • sneaky conversions (コソコソと勝手な型変換が行われる)

各 implicits の機能を Scala2 と Scala 3 で比べてみる.

文脈の引き回し

Scala 2 (implicit val / implicit parameter)

Scala 2 では implicit val でコンテキストとして引き回したい値を作り、 implicit parameter として自動的に関数に渡される.

/src/01_context/scala2.sc
#!/usr/bin/env -S scala-cli
//> using jvm "11"
//> using scala "2.13"

// Future の定義
// Future {
//   def apply[T](body: => T)(implicit executor: ExecutionContext): Future[T] = { ... }
// }

import scala.concurrent.{Future, ExecutionContext, Await}
import scala.concurrent.duration.Duration

object Main extends App {
  implicit val ec: ExecutionContext = ExecutionContext.global

  val myFuture = Future {
    println("Finding the answer...")
    42
  }

  println(Await.result(myFuture, Duration.Inf))
}

Main.main(Array())

Scala 3 (given / using)

Scala 3 では given でコンテキストとして引き回したい値を作り、 using で自動的に関数に渡される.

/src/01_context/scala3.sc
#!/usr/bin/env -S scala-cli
//>using jvm "11"
//>using scala "3.2"

// Future の定義
// Future {
//   def apply[T](body: => T)(using executor: ExecutionContext): Future[T] = { ... }
// }

import scala.concurrent.{Future, ExecutionContext, Await}
import scala.concurrent.duration.Duration

object Main extends App:
  given executor: ExecutionContext = ExecutionContext.global

  val myFuture = Future {
    println("Finding the answer...")
    42
  }

  println(Await.result(myFuture, Duration.Inf))

Main.main(Array())

したがって、 implicit val ←-→ givenimplicit parameter ←-→ using という対応関係がある.

Enricy my library パターン

Scala 2 (implicit class)

Scala 2 で Enrich my library パターンを使いたい場合、現在は implicit class が使われるのが普通. (昔は通常のクラス定義と implicit def による暗黙の型変換を組み合わせる必要があった)

/src/02_enrich_my_library/scala2.sc
#!/usr/bin/env -S scala-cli
//> using jvm "11"
//> using scala "2.13"

case class Book(title: String, pages: Int)

implicit class Qualifier(b: Book) {
  def qualifiesForChallenge: Boolean = b.pages > 20
}

object Main extends App {
  val book1 = Book("Winnie-the-Pooh", 160)
  println(book1.qualifiesForChallenge)        // true

  val book2 = Book("Too short", 5)
  println(book2.qualifiesForChallenge)        // false
}

Main.main(Array())

Scala 3 (extension)

Scala 3 では extension キーワードを使うことで拡張メソッドを定義することができ、これが Scala 2 における Enrich my library パターンに相当する.

/src/02_enrich_my_library/scala3.sc
#!/usr/bin/env -S scala-cli
//> using jvm "11"
//> using scala "3.2"

case class Book(title: String, pages: Int)

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

object Main extends App:
  val book1 = Book("Winnie-the-Pooh", 160)
  println(book1.qualifiesForChallenge)        // true
  val book2 = Book("Too short", 5)
  println(book2.qualifiesForChallenge)        // false

Main.main(Array())

型クラス (ad-hoc polymorphism)

Scala 2 (implicit val/def/object + implicit parameters / implicitly)

型クラスインスタンスを暗黙的に使用したい場合には、 implicit parameter として指定する以外に implicitly[T] メソッドを使うという方法もある. その際、型パラメータを [T: Scorable] のように指定するが、この記法は "context bound" と 呼ばれる. これは T 型に対して Scorable[T] 型の implicit val が定義されていることを強制するものである. (https://gist.github.com/ohsitab/453541)

/src/03_type_class/scala2.sc
#!/usr/bin/env -S scala-cli
//> using jvm "11"
//> using scala "2.13"

case class Book(title: String, pages: Int)
case class Article(title: String)

// 型クラス定義 (trait)
trait Scorable[T] {
  def score(t: T): Int
}

// 型クラスインスタンス定義 (implicit val)
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
}

// 型クラスインスタンスの使用 (implicit parameter)
// case class CurrentScore(value: Int) {
//   def addItem[T](item: T)(implicit scoring: Scorable[T]): CurrentScore =
//     CurrentScore(value + scoring.score(item))
// }

// 型クラスインスタンスの使用 (implicitly[T])
// [T: Scorable] という記法は "context bound" と呼ばれる.
// これは T 型に対して Scorable[T] 型の implicit val が定義されていることを強制するもの.
// <https://gist.github.com/ohsitab/453541>
case class CurrentScore(value: Int) {
  def addItem[T: Scorable](item: T): CurrentScore =
    CurrentScore(value + implicitly[Scorable[T]].score(item))
}

object Main extends App {
  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)

  println(initialScore)
  println(newScore)
}

Main.main(Array())

Scala 3 (given + using)

Scala 3 では implicitly[T] の代わりに summon[T] メソッドを使用することで implicit val を呼び出すことができる. context bound は Scala 2 の場合と同様に指定する.

なお、 Scala 2 の implicit val とは異なり、 given による型クラスインスタンスの生成は 名前を与えずに生成することが可能 になった. (anonymous given) 以下のコードは anonymous given となっているが、Scala 2 までと同様に明示的に名前を与えて定義することもできそうだ.

/src/03_type_class/scala3.sc
#!/usr/bin/env -S scala-cli
//> using jvm "11"
//> using scala "3.2"

case class Book(title: String, pages: Int)
case class Article(title: String)

// 型クラス (trait)
trait Scorable[T]:
  def score(t: T): Int

// 型クラスインスタンス (given)
given Scorable[Book] with
  override def score(b: Book): Int = b.pages

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

// 型クラスインスタンスの使用 (using)
// case class CurrentScore(value: Int):
//   def addItem[T](item: T)(using scoring: Scorable[T]): CurrentScore =
//     CurrentScore(value + scoring.score(item))

// 型クラスインスタンスの使用 (context bound + summon[T])
case class CurrentScore(value: Int):
  def addItem[T: Scorable](item: T): CurrentScore =
    CurrentScore(value + summon[Scorable[T]].score(item))

object Main extends App:
  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)
  println(initialScore)
  println(newScore)

Main.main(Array())

暗黙の型変換

Scala 2 (implicit def + import scala.language.implicitConversions)

/src/04_conversion/scala2.sc
#!/usr/bin/env -S scala-cli
//> using jvm "11"
//> using scala "2.13"

// Scala 2 で暗黙の型変換を行う場合、コンパイラにフラグを付与するか以下の import が必要.
import scala.language.implicitConversions

case class Book(title: String, pages: Int)
case class Item(value: String) extends AnyVal

// 暗黙の型変換が適用されることを期待しているメソッド
def print(item: Item): Unit = println(s"*** ${item.value} ***")

// 暗黙の型変換
implicit def bookToItem(book: Book): Item = Item(book.title)

object Main extends App {
  val book = Book("Dune", 600)
  print(book)
}

Main.main(Array())

Scala 3 (given + Conversion class)

/src/04_conversion/scala3.sc
#!/usr/bin/env -S scala-cli
//> using jvm "11"
//> using scala "3.2"

// Scala 3 でも以下の import かコンパイラにフラグを指定しないと警告が発生する.
import scala.language.implicitConversions

case class Book(title: String, pages: Int)
case class Item(value: String) extends AnyVal

// 暗黙の型変換が適用されることを期待しているメソッド
def print(item: Item): Unit = println(s"*** ${item.value} ***")

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

object Main extends App:
  val book = Book("Dune", 600)
  print(book)

Main.main(Array())

import の構文

Scala 2

Scala 2 では _ でワイルドカードインポートして implicit val / def / parameter をスコープに入れれば OK.

import scala.concurrent.ExecutionContext.Implicits.*

val future = Future(42)    // OK

Scala 3

Scala 3 ではワイルドカードインポートが に変わったが、 *単にワイルドカードインポートを行うだけでは given インスタンスを使用することができない. (import scala.concurrent.ExecutionContext.Implicits.* ではコンパイルエラーとなる.)

明示的に given をインポートすることで given インスタンスを using で使用することができるようになる.

import scala.concurrent.ExecutionContext.Implicits.given

val future = Future(42)    // OK
import scala.concurrent.ExecutionContext.Implicits.{given, *}

val future = Future(42)    // OK

まとめ

全体的に、動作原理ではなく各動作の意図が構文に反映されたと言える. さらに、 Scala 2 と比べて Scala 3 はコンパイラエラーも改善されデバッグが容易になっているようだ.