Scala 3 と比較して理解する Scala の implicit
この記事は Scala Advent Calandar 2022 の記事です.
Scala 2.x の implicit
はさまざまな使い道があるため少々ユーザーの混乱を招いていました. Scala 3 では implicit
が機能・目的に応じて整理されました. 一見複雑でわかりにくい Scala の implicit
を Scala 3 の機能と比較しながら理解してみましょう.
implicit 引数: グローバルなデータや Context のもちまわり
もっともわかりやすい(当社比) implicit の使い方は暗黙の引数です. 暗黙の引数はスコープにその引数の型の値が存在すればそれを自動で引数に取ってくれる機能です.
implicit 引数は設定、コネクションプールやスレッドプールのように、アプリケーション内部に何個も存在しない、さまざまなところで共有されうる値を楽に受け渡すために使えます. フロントエンドやモバイルのエンジニアにとってわかりやすい例でいうと React の props や Flutter の BuildContext でしょうか.
Scala の中で最も身近な暗黙の引数の利用例として ExecutionContext が挙げられます. Scala では、Future
を使うときに implicit ec: ExecutionContext
が必要です. ExecutionContext は非同期処理を効率的にスケジューリングするためのスレッドプールのようなものです.
開発者が積極的に ExecutionContext のインスタンスを触ることは少ないですが、Future
を使うメソッドは非同期処理のスケジューリングを裏側で ec
に移譲しているのでこの引数が常に必要になります.
def myAsyncFunction(arg:Int)(implicit ec: ExecutionContext):Future[Int] = ???
このようにメソッドのシグニチャに implicit ec: ExecutionContext
が現れますが、この ec
は便宜上つけた名前であまり意味を持ちません.
Scala 3 ではこの implicit
は using
に書き換えられます. また、using
節では変数の名前を省略できます.
def myAsyncFunction(arg:Int)(using ExecutionContext): Future[Int]= ???
さらに次のように書くこともできます. これは Contextual Function と呼ばれていて、 その名の通り文脈(下の例では ExecutionContext)を持った関数になります.
def myAsyncFunction(arg:Int): ExecutionContext ?=> Future[Int] = ???
import some.namespace.implicit(s).some.modules
Future
やそのほかのライブラリを使っていると次のような import
を目にすることがあると思います.
import cats.implicits._
import cats.effect.unsafe.implicit.global
import scala.concurrent.ExecutionContext.Implicits.global
これは、デフォルトのクラスや(この後説明する)型クラスのインスタンス、暗黙の型変換などを利用できるようにするためのコードです. 言語によっては prelude という名前が使われることもあります.
便利なメソッドを生やしたり、いい感じのデフォルト値を与えたりして開発者体験を改善するためのもので、初心者はあまり深入りせずこれを使うといい感じになるんやな〜くらいの温度感で見ておくといいと思います.
implicit class: 既存の型へメソッドを追加する(A.K.A 拡張メソッド)
やりたいこと
Int
に Int
から KiloMeter
クラスへ変換する km
メソッドを生やしたい.
val ikm = 1.km
Scala 2.x: 暗黙の変換 ・ enrich mylib パターン
Scala 2.x で既存のクラスにメソッドを足すには次のように書く必要がありました. これは enrich my library パターンなどと呼ばれています.
implicit class MyInt(i: Int) {
def km: KiloMeter = new KiloMeter(i)
}
case class KiloMeter(i:Int)
def program() = {
val i = 1
// implicit class MyInt が import されていたら
// ここで、Int => MyInt に変換されて MyInt#km が呼ばれるイメージ
val ikm = i.km // new MyInt(i).km
}
なお、上の implicit class
は以下の implicit def
のショートハンドです.
Int
から MyInt
へ暗黙の型変換が起こっています.
class MyInt(i: Int) {
def km: KiloMeter = new KiloMeter(i:Int)
}
implicit def int2myint(i:Int):MyInt = new MyInt(i)
Scala 3: extension method
Scala 3 では既存のクラスにメソッドを生やす専用の構文 extension
が用意されてより直感的に書けるようになりました.
extension (i:Int)
def twice(): Int = i * 2
もちろん Generics を使うこともできます.
extension [T] (t:T)
def show():Unit =
println(t)
@main def run() = 1.show() // 1
型クラス(あるいはアドホック多相)のインスタンスの定義
Rust や Haskell を使ったことがある人なら Rust の trait や Haskell の型クラスをイメージしてください.
Rust では fn f<T: ATrait>(a:T)
というふうに型パラメターにトレイト境界を指定することで、継承によらない形で多相、つまり異なる型について一般的に定義された関数を表現できます. また、これはコンパイル時に解決されるので不正な型を渡した場合はコンパイルエラーになります. trait の実装は(trait のインスタンスを implicit で与えることで)、ある型が何らかの制約を満たしていることをコンパイラに伝える役割を果たしています.
Ruby や Python のような動的型付け言語の Duck Typing のように柔軟ですが Duck Typing と違って、不正な入力をコンパイル時に検出できる嬉しさがあります.
まず説明のためにRust の trait をみてみましょう. Rust では関数の型パラメターに T : Trait1 : Trait2 : ...
とトレイト境界を指定することで、Trait1
,Trait2
, ... を impl
したものはなんでも 受け取る関数が表現できます.
trait Additive {
fn add(&self,other: Self);
}
impl Additive for /*Specific Type*/ {
// ???
}
// program は Additive trait を impl したあらゆる型を受け入れる.
fn program<T:Additive>(a:T,b:T) {
let it = a.add(b);
// do something with `it`
}
Scala 2.x: implicit def/val
Scala 2.x では以下のように trait
を定義して、implicit def
でインスタンスを作ることで trait
を実装できます. fn f(&self)
のような構文がないので trait
は型パラメターを取ります. また、implicit def
は Rust の impl ATrait for AType
に相当します.
なお、ここでいう trait
は Java の abstract class
や interface
よりも Rust の trait
や Haskell の型クラスに近いことに注意しましょう.
trait Additive[T] {
def add(a:T,b:T):T
}
// Rust の trait の impl に相当する.
implicit def additiveForKM = new Additive[KiloMeter] {
def add (a:KiloMeter, b: KiloMeter): KiloMeter = {
(a,b) match {
case (KiloMeter(a),KiloMeter(b)) => KiloMeter(a + b)
}
}
}
// program は Additive trait を impl したあらゆる型を受け入れるので additiveForKM
// がある KiloMeter も受けつける.
// trait 境界の表現. `T <: Additive` で表記するサブタイピングとは別であることに注意!
def program[T:Additive](a:T,b:T) = {
val it = implicitly[Additive[T]].add(a,b)
// do something with `it`
}
implicitly
は名前をつけないで型クラス(trait
) のインスタンスにアクセスするためのキーワードで implicit
で受け取る引数と対応しています.
つまり上の program
は以下のようにも書けます. いずれにしても implicit 引数の機能を利用して、 T
について Additive[T]
が実装されていることを要求しています.
def program[T](a:T,b:T)(implicit evidence: Additive[T]) = {
val it = evidence.add(a,b)
// do something with `it`
}
given/summon/using
Scala 3: Scala 3 では、型クラス(trait)のインスタンスを定義するのに使える given
というキーワードが入りました. 便宜上つけていた additiveForKM
という変数名も省略できるようになりました. また、implicitly
は summon
に置き換わってより直感的になっています.
trait Additive[T]:
def add(a:T,b:T): T
given Additive[KiloMeter] with
def add(a:KiloMeter,b:KiloMeter): KiloMeter =
(a,b) match
case (KiloMeter(a),KiloMeter(b)) => KiloMeter(a + b)
def program[T:Additive](a:T,b:T) =
val it = summon[Additive[T]].add(a,b)
// do something with `it`
最初に説明した extention
を以下のように定義するとよりそれっぽく、メソッドのように書けます.
extension[T:Addtive] (t0:T)
def add(t1:T):T = summon[Additive[T]].add(t0,t1)
def program[T:Additive](a:T,b:T) =
val it = a.add(b)
// do something with `it`
余談ですが、さらに contextual function, polymorphic function を使うと def program
は関数としても書けます. Haskell の =>
を ?=>
に、->
を =>
に置き換えたものとしてみるといいかもしれないです.
val program1 = [T] => (_:Additive[T]) ?=> (a:T) => (b:T) => a.add(b)
Scala 2.x では型引数や暗黙の引数を取る関数は Function 型で表現しにくかったですが、それが Scala 3 では解消されています.
いまいちアドホック多相(型クラス/trait)の嬉しさが分かりにくいかもしれないので別の例も出してみよう.
たとえば、以下のようにデータをシリアライズする関数を作ったとしましょう.
trait MySerializable:
def serialize = ???
trait MySerializableTC[T]:
def serialize(t:T) = ???
def mySerializerA[T<: MySerializable](t:T) = ???
def mySerializerB[T: MySerializableTC](t:T) = ???
mySerializerA
は Int
や Double
などのプリミティブ型、外部パッケージの型などに対応できませんが、mySerializerB
はプリミティブ型にも外部のパッケージの型にも対応できます.
(Int
はプリミティブ型で class Int extends MySerializable
とはできないことを思い出しましょう)
継承よりも緩い制約で多相性を表現できるということです. 嬉しいですね.
暗黙の変換
extension や型クラスの説明が終わったところで暗黙の変換についてみていきましょう.
Scala 2.x では、implicit def
で定義された、暗黙の変換をするメソッドがスコープにあるときには、型が合わないが暗黙の変換をすれば合うケースではその関数を呼び出して整合性を取ってくれました.
implicit def s2i(s:String) = Integer.parseInt(s)
def f(arg:Int): Unit = ???
def program() = {
f("1") // compile
}
Scala 3 では、この変換は implicit
ではなく、Conversion 型クラスの責務になりました.
String から Int への変換(Conversion) のインスタンスを与えてやることでコンパイラに暗黙の変換が可能であることを伝えているイメージです.
given Conversion[String, Int] with
def apply(s: String): Int = Integer.parseInt(s)
def f(arg:Int): Unit = ???
def program() = {
f("1") // compile
}
まとめ
以上のように Scala の implicit には 文脈の伝播・enrich my lib・型クラスの定義/呼び出し・暗黙の変換が混ざっていましたが、 Scala 3 ではそれぞれ Contextual Function, extension method, given/summon/using, Conversion に整理されました.
暗黙の変換の使い所は難しいですが、Contextual Function や extension method で以前よりすっきりとしたメソッド・関数の定義ができるようになっていたり、型クラスの概念も Scala 2.x と比べて扱いやすくなっていたりします.
それぞれに名前がついて機能が増えた/複雑になったようにも見えますが、コンパイル時に解決される暗黙の引数/変換という概念からさまざまな派生的な機能が提供されていて、それぞれにわかりやすさのためのエイリアスが貼られていると考えるととてもシンプルではないでしょうか.
Discussion