この記事を読むとわかること
- 継承ではなく
implicit
によって型の性質を表現できる - 型の性質によってその集合のメソッドを切り替えられる
- たとえば
Hoge[Foo]
とHoge[Bar]
に異なるメソッドを用意できる
- たとえば
導入
具体例として以下のような状況について考えてみる
- Twitter クライアントを作りたい
- マルチアカウントに対応させる
ミニマムの仕様としてこんな型を登場させる
trait UserAccount {
def accountId: Long
def name: String
}
trait FollowerTweet {
def tweetId: Long
def content: String
}
一覧表示のための集合も導入する
trait Sequence[+A]{
def length: Int
def findAt(position: Int): Option[A]
}
たとえばツイートの一覧表示では Sequence[FollowerTweet]
が利用されて、アカウントの一覧表示では Sequence[UserAccount]
が利用されるイメージだ
集合にメソッドを追加する
こんな形で呼び出したくなることはよくある
// DB から取得したアカウントの一覧
def accounts: Sequence[UserAccount]
// DB から取得したツイートの一覧
def tweets: Sequence[FollowerTweet]
// 切り替え先のアカウントを検索する
def accountExists(accountId: Long): Boolean = {
accounts.exists(_.accountId == accountId)
}
// フォロワーのツイートを検索する
def tweetExists(tweetId: Long): Boolean = {
tweets.exists(_.tweetId == tweetId)
}
全体の長さもそれぞれの位置の要素も分かっているのだから exists
を直接 Sequence
に定義することで
trait Sequence[+A]{
def length: Int
def findAt(position: Int): Option[A]
def exists(f: A => Boolean): Boolean = {
(0 to length - 1).view flatMap findAt exists f
}
}
解決できるかのように見えてここには罠がある
- 切り替えるアカウントの個数なんて多くても高々数十個
- つまり全件走査しても問題ないが
- ツイートの個数は数千件になるのか数万件になるのか全く不明
- なのでレスポンスがいつ返ってくるのか分からない
つまり exists
メソッドは全ての Sequence[A]
に対して呼べるものではないということだ
集合からメソッドを切り離す
trait Sequence[+A]{
def length: Int
def findAt(position: Int): Option[A]
}
trait SequenceTraverser[A]{
protected def underlying: Sequence[A]
def exists(f: A => Boolean): Boolean = {
(0 to underlying.length - 1).view flatMap underlying.findAt exists f
}
}
object UserAccount {
implicit class traverser(
override protected val underlying: Sequence[UserAccount])
extends SequenceTraverser[UserAccount]
}
この変更によって
-
accounts.exists(_.accountId == accountId)
はそのまま実行可能に -
tweets.exists(_.tweetId == tweetId)
はコンパイルエラーになる
型によってメソッドの有無を変えるところまでは実現できた。
ちなみにこのプロセスを分解すると下記のようになる
-
accounts.exists
メソッドのレシーバはSequence[UserAccount]
なので、コンパイラはコンパニオンオブジェクトUserAccount
からimplicit
を探しにいく- 結果として
exists
の定義されたSequenceTraverser
が見つかる
- 結果として
-
tweets.exists
も同様だがSequenceTraverser[FollowerTweet]
はどこにも存在しない- 結果として
exists
メソッドは見つからない
- 結果として
メソッドを型の性質によって分類する
UserAccount
専用の SequenceTraverser
が書かれているのはイマイチである。
なぜなら他にもこの exists
メソッドを必要とする型は無数に考えられるからだ
object Sequence {
implicit class TraverserImpl[A: HasShortLength](
override protected val underlying: Sequence[A]) extends SequenceTraverser[A]
}
trait HasShortLength[A]
今度は SequenceTraverser
の実装を Sequence
に移して、新しく HasShortLength
という型を導入した。
これは文字通り、長さが十分に短いという性質を表現するための型だ
object UserAccount {
implicit object short extends HasShortLength[UserAccount]
}
UserAccount
には「長さが短い」ことを示すための最低限の記述のみが残った。
この変更でも
-
accounts.exists(_.accountId == accountId)
は実行可能 -
tweets.exists(_.tweetId == tweetId)
はコンパイルエラー
という条件はもちろん守られている。
今回のプロセスでは implicit
の探索が二段階あることに注意したい
-
accounts.exists
のレシーバSequence[UserAccount]
からSequence
のコンパニオンオブジェクトが探索される -
TraverserImpl[A: HasShortLength]
からHasShortLength[UserAccount]
を見つけるためにUserAccount
のコンパニオンオブジェクトが探索される
他の型についても全く同様に :
HasShortLength
によって「長さが短い」ことをコンパイラに示すことができる
/* 直近数件のツイート */
trait RecentTweet {
...
}
object RecentTweet {
implicit object short extends HasShortLength[RecentTweet]
}
この場合では Sequence[RecentTweet]
から exists
を呼び出せるようになっている
用語のヒント
-
[A: HasShortLength]
は context bounds と呼ばれる構文糖- 中身は implicit parameter
- だから
implicit
が探索される
- だから
- この
HasShortLength
は型クラス-
object short
がHasShortLength
のインスタンス
-
- 中身は implicit parameter
-
exists
を呼び出せるのは型クラスではなく、いわゆる implicit conversion と呼ばれる機能のおかげ- 今回の型クラスはあくまでレシーバの型を制限するために機能している
おわり
ちょっと主張したかったことも最後に
- 型クラスなら従来の継承ベースでは不可能だった設計が可能になる
- 今回の例では
UserAccount
型にもSequence
型にも全く手を加えてないのがポイント- つまりこれは一部の設計マニアのための機能ではないし黒魔術でも何でもない!
- 今回の例では
-
implicit
は必ずしも危険ではないので毛嫌いしないでおこう- 暗黙の型変換という和訳がミスリードの原因だろうか
- 全てユーザが明示的に指定しているのだから暗黙ではない!
- 暗黙の型変換という和訳がミスリードの原因だろうか