Scala 2 の implicits と Scala 3 の対応機能
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 として自動的に関数に渡される.
#!/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 で自動的に関数に渡される.
#!/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 ←-→ given 、 implicit parameter ←-→ using という対応関係がある.
Enricy my library パターン
Scala 2 (implicit class)
Scala 2 で Enrich my library パターンを使いたい場合、現在は implicit class が使われるのが普通.
(昔は通常のクラス定義と implicit def による暗黙の型変換を組み合わせる必要があった)
#!/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 パターンに相当する.
#!/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)
#!/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 までと同様に明示的に名前を与えて定義することもできそうだ.
#!/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)
#!/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)
#!/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 はコンパイラエラーも改善されデバッグが容易になっているようだ.