Haskellの型クラスは知っていたけどScalaの型クラスがよくわからなかったのでググッて調べているうちに、CatsというScalaライブラリの公式でとてもコンパクトな説明を見つけました。
自分の理解の助けも兼ねて翻訳しました。
用語の定義
型クラスはもともと関数型言語の概念なので、オブジェクト指向と同じ言葉でも意味が異なることが多いです。
本文を読む際に注意してください。
| 用語 | 意味 | 本文中の例 |
|---|---|---|
| 型 | データを保持する器。 | String |
| 型クラス、クラス | ある型に振る舞いを強制する(=特定の関数の引数になれることを保証する)仕組み。オブジェクト指向のクラスとは別物 | Show |
| インスタンス | 型クラスによって振る舞いを規定されている型。オブジェクト指向のインスタンスとは別物 | Show[String] |
本文
型クラス
型クラスパターンとは、ある型に新しい振る舞いを与える方法で、Scalaのあらゆるところに顔を出します。こう言うと、Javaで言うところの"インターフェース"みたいなものだと思うかもしれませんね。例を見てみましょう。
/**
* テキストとして表現する方法を提供する型クラス
*/
trait Show[A] {
def show(f: A): String
}
この型クラスが言わんとしているのは、Show[A]型の値は、AをStringに変換する方法を持っているということです。それでは、AをStringに変換する多相的な関数を定義してみます。
def log[A](a: A)(implicit s: Show[A]) = println(s.show(a))
まだShowインスタンスを定義していない状態でlog関数を呼ぼうとすると、コンパイルエラーが出ます。
scala> log("a string")
<console>:15: error: could not find implicit value for parameter s: Show[String]
log("a string")
^
エラーを消すためにStringをShowのインスタンスにするのは簡単です。
implicit val stringShow = new Show[String] {
def show(s: String) = s
}
これで、logの呼び出しが成功しました。
scala> log("a string")
a string
この例は、型クラスパターンの強力さを物語っています。Java.lang.Stringに新しいインターフェースを実装して振る舞いを増やすことはできません。そこで私たちは、既存のJava.lang.Stringを拡張することなしにStringに対するShowの実装を成し遂げたのです。このパターンは、既存の型に新しい振る舞いを組み込めます。このやり方はよく"アドホック多相"と呼ばれます。
ある型に関しては、別の型のShowインスタンスを用意しないとその型のShowインスタンスを作れないこともあります。例として、OptionをShowのインスタンスにしてみましょう。
implicit def optionShow[A](implicit sa: Show[A]) = new Show[Option[A]] {
def show(oa: Option[A]): String = oa match {
case None => "None"
case Some(a) => "Some("+ sa.show(a) + ")"
}
}
こうして、Option[String]やOption[Option[String]]に対してlogを適用できるようになりました。
scala> log(Option(Option("hello")))
Some(Some(hello))
Scalaは頻出パターンのための糖衣構文を用意しています。
def log[A: Show](a: A) = println(implicitly[Show[A]].show(a))
これは下のコードと同義です。
def log[A](a: A)(implicit s: Show[A]) = println(s.show(a))
A : Showをパラメータの型に指定すると、暗黙的なパラメータがシグネチャに追加されます(パラメータの名前は私達からはわかりません)。