読者です 読者をやめる 読者になる 読者になる

で、結局型クラスって何がうれしいの、ということの説明

programming Scala

前回の記事では、Scala では implicit parameter を利用することで型クラスと同等のことが実現できることがわかりました。しかし前回の疑問として、「で、それの何がうれしいのよ」というのは残っていましたね。

今回はそのうれしみをなるべくわかりやすく説明したいと思います。

たとえば、色んなクラスに flipFlap メソッドを生やしたい

まず、前回と同じく「値をひっくり返す」という flipFlap について考えてみましょう。

前回は関数を作りましたが、Scalaらしく、こういう動きにしたいですね

1.flipFlap // => -1
true.flipFlap // => false
"string".flipFlap // => gnirts

Scala ではクラスを拡張するときには implicit conversion を使うのが一般的ですね。

class IntFlipFlapWrapper(x:Int) {
  def flipFlap(x: Int) = - x
}
implicit def toIntFlipFlapWrapper(x: Int) = new IntFlipFlapWrapper(x)

これで、1.flipFlap のようなステートメントを見つけると、コンパイラは「ん、Int には flipFlap なんてメソッドはないぞ?」となり、「implicit conversion でなんか別のクラスにならないかな?」と implicit conversion を探しにいきます。すると、toIntFlipFlapWrapper というメソッドが見つかりますね。引数に Int をとり、別のクラスに変換するメソッドが implicit で定義されているやつです。このとき、変換先である「IntFlipFlapWrapper」がflipFlapメソッドを持っているので、1 はコンパイラにより IntFlipFlapWrapper に変換されて、IntFlipFlapWrapperのflipFlapメソッドが呼び出される、という流れです。

さて、ここまでは問題ありませんが、flipFlap 可能な型すべてに対して implicit conversion を定義するのはちょっとエレガントじゃないですね。あと、「flipFlap 可能な型の集合」ってのがソースコード中のどこでも示されていなくて、整理されていない感じになってしまいそうです。

そこで型クラスですよ

ところで、型クラスというのは「ある計算が可能な型の集合を定義する」ためのものでした。これはめっちゃこの用途にマッチする気がしましね。では実際にやってみましょう。

// FlipFlap型クラスを定義
trait FlipFlap[T] {
  def flipFlap(x: T): T
} 

// Int をFlipFlap型クラスの型インスタンスとして定義
implicit val intFlipFlap = new FlipFlap[Int] {
  def flipFlap(x: Int): Int = - x
}

// String をFlipFlap型クラスのインスタンスとして定義
implicit val stringFlipFlap = new FlipFlap[String] {
  def flipFlap(x: String): String = x.reverse
}

ここまでは前回と一緒です。前回 implicit object で定義してた型インスタンスを implicit val で定義してますが、やってることは一緒です。flipFlap メソッドポリモーフィズムさせるためのオブジェクトたちを implicit で作ってるだけです。

さて、ではこいつを利用して、FlipFlap型クラスのインスタンスであるところのInt, String を一気に拡張してみましょう。

型クラスを利用してそのインスタンスを一気に拡張する

implicit conversion でクラスを拡張するためには、まずは変換先のクラスを作らないといけませんね。でも、今回は具体的な型ではなくて、たくさんの型に一気に対応しないとダメなので、具体的なクラスではなくて、型パラメータをともなった trait で作りましょう。

trait FlipFlapWrapper[A] {
  val f:FlipFlap[A] // ポリモーフィズムするためのオブジェクト
  val v:A // 拡張される側の値
  def flipFlap():A = f.flipFlap(v)
}

def flipFlap が、生やしたいメソッドです。こいつは f に対して処理を委譲しているだけですね。で、肝心の f は、もし [A] が [Int] の場合 f は FlipFlap[Int] となります。もし[A]が[String]なら、FlipFlap[String]になりますね、そんな感じで、[A]が変わるとflipFlap メソッドの移譲先の型も決定し、ポリモーフィズムする仕組みになっています。ではこいつを利用して implicit conversion のための implicit def を定義してやりましょう。

implicit def toFlipFlapWrapper[A](value:A)(implicit implicitFlipFlap:FlipFlap[A]) = new FlipFlapWrapper[A] {
  val v = value
  val f = implicitFlipFlap
}

こんな感じです。さて、この時点で、1.flipFlap するとどうなるでしょうか?Int には flipFlap が生えていないので、コンパイラは implicit conversion を探しに行きます。で、toFlipFlapWrapper が見つかります。引数に Int が渡されたので、型推論により toFlipFlapWrapperの[A]が[Int]に確定しますね。そうすると implicitFlipFlap の型も自動的に FlipFlap[Int] に確定し、intFlapFlap が暗黙的に束縛されます。これは前回見たのと同様の理屈ですね。さて、そうなると、このときもとの "1" という値は、FlipFlapWrapper の v に 1 が、 f に intFlapFlap が束縛されたオブジェクトに変換されることになります。結果、1.flipFlapintFlipFlap.flipFlap(1) を計算することになり、-1 を返します。

では"string".flipFlapするとどうなるでしょうか。同じ理屈で、"string" は f にはstringFlipFlapが、v に "string" が束縛された FlipFlapWrapperに変換されます。結果、"string".flipFlapstringFlipFlap.flipFlap("string") が計算され、"gnirts"が返ってきます。

では例によって、Boolean もFlipFlap型クラスのインスタンスにしてみましょう。

// Boolean をFlipFlap型クラスの型インスタンスとして定義
implicit val booleanFlipFlap = new FlipFlap[Boolean] {
  def flipFlap(x: Boolean): Boolean = ! x
}

これでいいですね。

さて、 true.flipFlap してみます。今までと同じ理屈で、implicit conversion が走り、true.flipFlap は FlipFlapWrapper の v が true に、f が booleanFlipFlap に束縛されたものに変換されます。結果、booleanFlipFlap.flipFlap(true)が計算されることになり、false が返ります。

こんな感じで、「flipFlap可能である」という同じ特徴を持つ型の集合を、FlipFlap型クラスのインスタンスにして、それらを利用した implicit conversion を定義することで、FlipFlap型クラスのインスタンスすべてを同時に拡張することができました。

まとめ

型クラスを利用することで、共通の性質を持つ型を型クラスにまとめた上で、その型すべてを拡張することができてうれしい、ということが言えそうです。おもしろいのは、今回で言えば Boolean も Int も String も、共通の祖先クラスを持っていない上、組み込みクラスであることですね。自分で作ったクラスに対して共通の性質を付与するならば、同じ trait を継承するという手段はありますが、組み込みクラスにあとから新しく祖先クラスを追加することはできません。伝統的なオブジェクト指向のタイプヒエラルキーよりも、型クラスと型インスタンスによるヒエラルキーのほうが柔軟であることが見て取れたかと思います。

追記

というわけなので、これで型クラスのうれしみがすべて説明されているわけではないのでまだまだ学習が必要ですね。道は険しい。