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
をパラメータの型に指定すると、暗黙的なパラメータがシグネチャに追加されます(パラメータの名前は私達からはわかりません)。