LoginSignup
11
11

More than 1 year has passed since last update.

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

Last updated at Posted at 2016-05-13

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

  • 継承ではなく 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 shortHasShortLength のインスタンス
  • exists を呼び出せるのは型クラスではなく、いわゆる implicit conversion と呼ばれる機能のおかげ
    • 今回の型クラスはあくまでレシーバの型を制限するために機能している

おわり

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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
11