Scala implicit デザインパターン
Scala implicit デザインパターン
「implicit
。書いてあるコードは読めるけど自分で実装する時に使いどころがワカン。」 みたいのがあって職場の人に聞いたらいい感じのリンクを教えて頂いたので翻訳しつつ勉強がてらメモ。
目次
- 最初に
- Implicit Contexts
- Type-class Implicits
- Derived Implicits
- Type-driving Implicits
- まとめ
最初に
しばしば貧弱Scalaエンジニア(俺)達から畏敬の念とともに語られるimplicit
。実はそれ自体の機能はそんなに強力じゃないみたい。
implicit parameter
: 明示的に引数のを渡す必要なく、その型とスコープ内の値に基づいて自動的に推論implicit conversion function
: 要求に応じて明示的に関数を呼び出す。
ただ単純に使用するのではなく、長い歴史と数多くのエンジニアたちの知恵を礎に最適な使用方法(=デザインパターン)が生まれた。4つほど紹介してみる。
Implicit Contexts
DB Connectionとか 何回も何回も使用する引数を、渡す度に定義してたらめんどくさいよね。値があるなら空気読んでそれ使ってよ。 という話。
ex) 非同期処理する全てのメソッドにExecutionContextを引き渡す
// 全てのメソッドが`ExecutionContext`を必要としている def getEmployee(id: Int)(implicit e: ExecutionContext): Future[Employee] = ??? def getRole(employee :Employee)(implicit e: ExecutionContext): Future[Role] = ???
getEmployee()
, getRole()
の両方でカリー化された引数でExecutionContext
を要求している。
// implicitではない雑魚 val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global val bigEmployee: Future[EmployeeWithRole] = getEmployee(100)(ec).flatMap { e => getRole(e)(ec).map { r => EmployeeWithRole(e.id, e.name,r) }(ec) }(ec)
implicitを使わない場合全ての呼び出し箇所で明示的に(ec)
を引き渡す必要がある。すごくめんどくさい。 4つならまだいいけどもっと数が増えてきたり、複数箇所で使用されていたらもうめんどくさすぎる。
// implicit parameterを使うぞ! implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global val bigEmployee: Future[EmployeeWithRole] = getEmployee(100).flatMap { e => getRole(e).map { r => EmployeeWithRole(e.id, e.name,r) } }
implicit
キーワードとともにExecutionContext
を定義することで、全ての使用箇所で明示的な引数の引き渡し(ec)
を全て削除できる。やったねたえちゃん!という話。
Implicit Contextsの定義
- 通常ジェネリックス型ではなく、型パラメーターも持たない。
- 異なるシグネチャを持つあらゆる関数に同じ
implicit
が渡される。 implicit
は呼び出すタイミングによってその都度値が評価される。- play frameworkではHTTPリクエストの度に
Request
オブジェクトの値が変わる
- play frameworkではHTTPリクエストの度に
- AkkaのActerSystemのような場合には
implicit
の値は変更可能な場合がある。(これは意味不明誰か教えてくれ)
Implicit Contexts
パターンは "ボイラープレートな引数を渡す便利な方法" であり、implicit parameter
の唯一使用方法である。それ以外は一般的な引数と違いはない。
Type-class Implicits
いろんな型からJsonに変換したい場合とかにパターンマッチやオーバーロードで実装もできるけど、全ての型を記述するのは不可能でなによりコンパイル時にわからないし、同名関数を定義しまくるのは汎用性がない。変換可能な型が事前にわかればコンパイルエラーで防げるし、呼ばれるべき関数が型でわかれば汎用性もあるよねという話。
// Jsonオブジェクトの定義 sealed trait Json object Json{ case class Str(s: String) extends Json case class Num(value: Double) extends Json // ... もっとたくさん } def convertToJson(x) // このメソッドで x をJsonオブジェクトに変換したい
このような場合 def convertToJson(x): Json
の実装はどのようなパターンが考えられるだろうか。
パターンマッチ
まずは前述のパターンマッチで実装してみる。
// パターンマッチで convertToJson を実装してみる def convertToJson(x: Any): Json = { x match{ case s: String => Json.Str(s) case d: Double => Json.Num(d) case i: Int => Json.Num(i.toDouble) // float, short などさらに多くのケースが考えられる } }
さあ。このconvertToJson()
はどうだろうか...?勿論パターンマッチで実装済みの型からの変換は正しく動くだろう。しかし、未実装の型(Boolean
やjava.io.File
)の場合はどうだろ? 実行時エラーで失敗してしまう。 現実的に考えて全ての型のパターンマッチを実装するのは不可能であるし、何より変換できない型が渡された場合はコンパイラーに止めてほしい。この問題を解決するのがType-class Implicits
パターンだ。
オーバーロード
オーバーロードを使用することで複数パターンのconvertToJson()
を実装することができるかもしれない。
def convertToJson(t: String) = Json.Str(t) def convertToJson(t: Double) = Json.Num(t) def convertToJson(t: Int) = Json.Num(t.toDouble)
このように同名で引数の型が異なる関数を複数用意しておけば、前述の未実装の場合にコンパイルエラーが発生しない問題は回避できる。
しかし、convertToJson()
のような関数を複数作成しようとした場合どうなるだろうか?
def convertToJson(t: String) = Json.Str(t) def convertToJson(t: Double) = Json.Num(t) def convertToJson(t: Int) = Json.Num(t.toDouble) def convertToJsonAndPrint(t: String) = println(convertToJson(t)) def convertToJsonAndPrint(t: Double) = println(convertToJson(t)) def convertToJsonAndPrint(t: Int) = println(convertToJson(t)) def convertMultipleItemsToJson(t: Array[String]) = t.map(convertToJson) def convertMultipleItemsToJson(t: Array[Double]) = t.map(convertToJson) def convertMultipleItemsToJson(t: Array[Int]) = t.map(convertToJson)
こうなる。
オーバーロードは同名関数という規約があるので、少し役割を変えた関数を定義しようとした場合。その新しい関数でも全ての型を網羅するオーバーライドを記述する必要がある。これはもうさいあくである。
Type-class Implicits パターン
- パターンマッチは全パターンを網羅することができない
- オーバーロードは汎用性がない
ここでType-class Implicits
パターンを使用してみよう!
trait Jsonable[T]{ def serialize(t: T): Json } object Jsonable{ implicit object StringJsonable extends Jsonable[String]{ def serialize(t: String) = Json.Str(t) } implicit object DoubleJsonable extends Jsonable[Double]{ def serialize(t: Double) = Json.Num(t) } implicit object IntJsonable extends Jsonable[Int]{ def serialize(t: Int) = Json.Num(t.toDouble) } }
こん感じで書いておけば...
def convertToJson[T](x: T)(implicit converter: Jsonable[T]): Json = {
converter.serialize(x)
}
このようにconvertToJson()
を実装できる!!!!
convertToJson()
のカリー化された引数にimplicit
でJsonable[T]
型を受け取っている。この[T]
はconvertToJson[T]
で束縛されている。
例えばx
がString
であった場合、このような挙動になる(っぽい)。
x
がString
なのでJsonable[T]
もJsonable[String]
に束縛されるJsonable[String]
はimplicit
なのでscalaが使えるimplicitがないか捜索の度に出る- まずスコープ内を探す、この場合は定義していないので見つからない
- 次は
Jsonable
のコンパニオンオブジェクトを探しに行く、今回はStringJsonable
を定義済みなのでここで見つける - scala「お!定義済みだね!通れ!」となる
もし、Jsonable[File]
のような未定義のクラスの場合。scala「だめだ!お前は!implicitが見つからないぞ!コンパイルエラーだ」となる。
さらに、前述の def convertToJsonAndPrint(t: String) = println(convertToJson(t))
のような関数を定義した場合も「お!StringからJsonに変換するimplicitがほしいんだね!StringJsonableを使いな!」となる。やったね!
Type-class Implicitsと型クラスimplicitの違い
Type-class Implicits
は評価する度に使用するimplicitを探しにいく、ライブラリで実装済のものを使う場合もあれば、自前で実装した物を使用する場合もある。Type-class Implicits
は通常、データを保有しており、しばしば変更可能型クラスimplicitは純粋な関数である場合が多い。(まったく理解していない)
Derived Implicits
Type-class Implicits
では深い階層の呼び出しも型安全に行えるよ!イエ〜イ! という話。
前述のJsonクラスでStr
やNum
の他にJson List
も実装したいとしたらどうすれば良いだろうか?
sealed trait Json object Json { case class Str(s: String) extends Json case class Num(value: Double) extends Json // ... and more }
Str
とNum
は実装済みであるのでconvertToJson()
は成功させたい。しかし、File
型などの未実装の場合はconvertToJson()
でコンパイルエラーにして防ぎたい。
// これは実行できる convertToJson(Seq(1, 2, 3)) convertToJson(Seq("baz", "bar", "foo"))
// これはコンパイルエラーにしたい convertToJson(Seq(new java.io.File("."), new java.io.File("/")))
どうすればこの問題を解決できるだろうか?
答えは先ほど追加したJsonable
にSeqをserializeするimplicit SeqJsonable
を作成。このSeqJsonable
内のSeqを[T]
型に束縛してしまえばいいのだ。
きっと日本語でおkなのでコードを見てみよう!
trait Jsonable[T]{ def serialize(t: T): Json } object Jsonable{ implicit object StringJsonable extends Jsonable[String]{ def serialize(t: String) = Json.Str(t) } implicit object DoubleJsonable extends Jsonable[Double]{ def serialize(t: Double) = Json.Num(t) } implicit object IntJsonable extends Jsonable[Int]{ def serialize(t: Int) = Json.Num(t.toDouble) } // このSeqJsonableを新しく追加 implicit def SeqJsonable[T: Jsonable]: Jsonable[Seq[T]] = new Jsonable[Seq[T]]{ def serialize(t: Seq[T]) = { Json.List(t.map(implicitly[Jsonable[T]].serialize):_*) } } }
このdef serialize(t: Seq[T])
の[T]
型の束縛のおかげで、implicit
実装済みの型は呼び出せますが、未実装のjava.io.File
型などは呼び出し時にコンパイルエラーになります!めでたし、めでたし。
実はDerived Implicits
パターンはこれだけではありません。どれだけネストした型でも型安全に呼び出すことができるのです!
これも日本語でおkなのでコードを見ていきましょう!
// コンパイル可能 convertToJson(Seq(Seq(Seq(1, 2, 3)))) // コンパイルエラー convertToJson(Seq(Seq(Seq(new java.io.File(".")))))
どれだけネストが深くても関係ありません。コンパイラが再帰的に型を判定し[T]
型にマッチしなければコンパイルエラーに変換します。
この制約は一見面倒に感じるかもしれません。仮にJsonable
が外部ライブラリだった場合、前述のようにjava.io.File
型はコンパイルエラーなってしまうため実行できないからです。しかし、これはメリットでもあります。
存在しなければjava.io.File
型のFileJsonable
を実装すれば、Seq[File]
も同様に扱うことができるのです。
// これを作ってあげる implicit object FileJsonable extends Jsonable[java.io.File]{ def serialize(t: java.io.File) = Json.Str(t.getAbsolutePath) } // これも実行可能になる convertToJson(Seq(Seq(Seq(new java.io.File(".")))))
このように型の束縛は実装を制限するのではなく、ユーザー側に拡張性という大きなメリットをもたらします。
Type-driving Implicits
より大きな型に変換しようとすると予想外の動きをすることがあるけど、一つの関数が引数の型に応じて戻りの型を自動で変換してくれた楽チンだよね! という話
ex) Byte -> Short -> Longに変換
// Byteで定義 val x: Byte = 123 x: Byte = 123 // Byte -> Shortに変換 val y: Short = x y: Short = 123 // Short -> Longに変換 val z: Long = y z: Long = 123L
これは結構便利。しかし、予想外の動きをする場合があります。
// Longの最大値 val bigLong = Long.MaxValue - 1 bigLong: Long = 9223372036854775806L // Long -> Floatに変換 val bigFloat: Float = bigLong bigFloat: Float = 9.223372E18F // Float -> Longに戻す val bigLong2: Long = bigFloat.toLong bigLong2: Long = 9223372036854775807L // 最初のLongと一度Floatを通したLongを比較 bigLong == bigLong2 > res40: Boolean = false
false!!!!!!!
このような事態を防ぐにはどうすれば良いでしょう?型の拡張を自動で行う関数を定義できます!
{ def widen[T, V](x: T)(implicit widener: Widener[T, V]): V = widener.widen(x) class Widener[T, V](val widen: T => V) object Widener{ implicit object FloatWiden extends Widener[Float, Double](_.toDouble) implicit object ByteWiden extends Widener[Byte, Short](_.toShort) implicit object ShortWiden extends Widener[Short, Int](_.toInt) implicit object IntWiden extends Widener[Int, Long](_.toLong) } }
こんな感じで、与えられた型を自動で拡張するwiden()
関数を作成します!さぁ呼び出してみましょう!
// Floatを渡すとDoubleが返ってくる widen(1.23f: Float) > res12: Double = 1.2300000190734863 val smallValue: Byte = 123 // Byteを渡すとShortが返ってくる val shortValue = widen(smallValue) > shortValue: Short = 123 // Shortを渡すとIntが返ってくる val intValue = widen(shortValue) > intValue: Int = 123 // Intを渡すとLongが返ってくる val longValue = widen(intValue) > longValue: Long = 123L
戻りの型は最初のようにscalaのコンパイラが自動で判断するのではなく、Widener
に定義されているimplicit
の実装で決定されます。それだけではなく、widen(longValue: Long)
このように未実装な型を渡した場合はコンパイルエラーで防ぐことができます。
このDerived Implicits
は、一般的にTuple
を大きなTuple
に拡張する時にも用いられます。
{ def extend[T, V, R](tuple: T, value: V)(implicit extender: Extender[T, V, R]): R = { extender.extend(tuple, value) } } case class Extender[T, V, R](val extend: (T, V) => R) object Extender { implicit def tuple2[T1, T2, V, R] = Extender[(T1, T2), V, (T1, T2, V)] { case ((t1, t2), v) => (t1, t2, v) } implicit def tuple3[T1, T2, T3, V, R] = Extender[(T1, T2, T3), V, (T1, T2, T3, V)]{ case ((t1, t2, t3), v) => (t1, t2, t3, v) } implicit def tuple4[T1, T2, T3, T4, V, R] = Extender[(T1, T2, T3, T4), V, (T1, T2, T3, T4, V)]{ case ((t1, t2, t3, t4), v) => (t1, t2, t3, t4, v) } // ... tuple21 まで続くよ ... }
widen()
の時と同じで、戻り値の型はExtender
で定義したimplicit
に依存します。結果、Tuple2
とTuple3
はクラス階層では互いに直接関係がありませんので、Tuple2
でextend()
を使用することができ、scalaコンパイラは結果をTuple3
であると自動的に判断します。
// Tuple2を宣言 val t = (1, "lol") t: (Int, String) = (1, "lol") // Tuple3に拡張 val bigger = extend(t, true) bigger: (Int, String, Boolean) = (1, "lol", true) // Tuple4に拡張 al evenBigger = extend(bigger, List()) evenBigger: (Int, String, Boolean, List[Nothing]) = (1, "lol", true, List())
Type-driving Implicits
パターンを使用することで、implicit
パラメータのどのインスタンスが有効範囲にあるかに応じて、関数の戻り値の型をscalaコンパイラが推測する方法を制御できる。これは少しクレーバーなテクニックです。戻りの方を制御するのは気づかない可能性もありますし何でもかんでもType-driving Implicits
パターンを使用すれば良いというわけではありません。しかし、効果的に使用すれば非常に有効なパターンになります。
ex) fastparse ではこのExtender
に似た形で実装されています。
最後に
これでimplicit
を使用したいくつかのデザインパターンの紹介は終わりになります。しかし、これで全てを説明しきったわけではありません。ここで説明しなかった使用方法を最後に少し言及します。
- Shapeless の Aux Pattern。
Shapeless
ではimplicit
を使用してできるほぼ全てのことが網羅されています。しかし、ここで全てに言及するのは不可能なので興味があれば除いて見てみてください。 implicit parameters
の代わりにimplicit conversions
を使用するもの。 ex)implicit constructors
やその拡張クラスを実装するもの。(C#のものに似ている)- 前述のCollectionのようなものだけではなく、
case class
で動作するDerived Implicits
パターンも存在します。Shapelessでも実装されています。 Implicit Contexts
はよりパワフルに、行番号やファイル名などコンパイラからさらなる情報を取得することもできます。ClassTag や sourcecodeで実装されています。
以上です!!マサカリ待っています!
参考
http://www.lihaoyi.com/post/ImplicitDesignPatternsinScala.html