呼び出せるメソッドを型によって切り替える方法 on Scala


## この記事を読むとわかること

* 継承ではなく `implicit` によって型の性質を表現できる
* 型の性質によってその集合のメソッドを切り替えられる
  * たとえば `Hoge[Foo]` と `Hoge[Bar]` に異なるメソッドを用意できる

## 導入

具体例として以下のような状況について考えてみる

* Twitter クライアントを作りたい
* マルチアカウントに対応させる

ミニマムの仕様としてこんな型を登場させる

```scala
trait UserAccount {
  def accountId: Long
  def name: String
}
trait FollowerTweet {
  def tweetId: Long
  def content: String
}
```

一覧表示のための集合も導入する

```scala
trait Sequence[+A]{
  def length: Int
  def findAt(position: Int): Option[A]
}
```

たとえばツイートの一覧表示では `Sequence[FollowerTweet]` が利用されて、アカウントの一覧表示では `Sequence[UserAccount]` が利用されるイメージだ

## 集合にメソッドを追加する

こんな形で呼び出したくなることはよくある

```scala
// 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` に定義することで

```scala
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]` に対して呼べるものではないということだ

## 集合からメソッドを切り離す

```scala
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` メソッドを必要とする型は無数に考えられるからだ

```scala
object Sequence {
  implicit class TraverserImpl[A: HasShortLength](
    override protected val underlying: Sequence[A]) extends SequenceTraverser[A]
}
trait HasShortLength[A]
```

今度は `SequenceTraverser` の実装を `Sequence` に移して、新しく `HasShortLength` という型を導入した。
これは文字通り、長さが十分に短いという性質を表現するための型だ

```scala
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` によって「長さが短い」ことをコンパイラに示すことができる

```scala
/* 直近数件のツイート */
trait RecentTweet {
  ...
}
object RecentTweet {
  implicit object short extends HasShortLength[RecentTweet]
}
```

この場合では `Sequence[RecentTweet]` から `exists` を呼び出せるようになっている


## 用語のヒント

* `[A: HasShortLength]` は context bounds と呼ばれる構文糖
  * 中身は implicit parameter
      * だから `implicit` が探索される
  * この `HasShortLength` は型クラス
      * `object short` が `HasShortLength` のインスタンス
* `exists` を呼び出せるのは型クラスではなく、いわゆる implicit conversion と呼ばれる機能のおかげ
  * 今回の型クラスはあくまでレシーバの型を制限するために機能している

## おわり

ちょっと主張したかったことも最後に

* 型クラスなら従来の継承ベースでは不可能だった設計が可能になる
  * 今回の例では `UserAccount` 型にも `Sequence` 型にも全く手を加えてないのがポイント
     * つまりこれは一部の設計マニアのための機能ではないし黒魔術でも何でもない！
* `implicit` は必ずしも危険ではないので毛嫌いしないでおこう
  * 暗黙の型変換という和訳がミスリードの原因だろうか
     * 全てユーザが明示的に指定しているのだから暗黙ではない！