関数型プログラミング入門(5) - 型クラス

いま、あるクラス(たとえば座標を表すPoint)があって、そのインスタンスの文字列表現を出力するための共通の方法を実装したいということを考えます。これはJavaやC#を使ったことのある人たちならば容易に想像がつくでしょう。そうです。toStringですね。本記事ではtoStringと同じような仕組みを考えたいと思います。

ここで、文字列表現を出力できる値に対して統一的にshowsというメソッドを通じて文字列表現を出力する、ということを仕様としましょう。

たぶんオブジェクト指向に慣れ親しんだ人であれば以下のように書きたくなりますよね。

trait Showable { def shows: String }
case class Point(x: Int, y: Int) extends Showable { def shows: String = s"($x, $y)" }

しかし、この方法には「多相性(ポリモーフィズム)を実現させるためにはShowableをすべてのクラスで実装しなければならない」という欠点があります。何で?そんなの当たり前の事じゃないの?と思う方はオブジェクト指向をよく御存じの方でしょう。

ではこういうのはどうでしょうか。ShowableをたとえばJava標準ライブラリのBigIntegerに実装させたいときはどのようにすればいいでしょうか?あるいは、オブジェクトの内容をTweetできるtweetメソッドを持ったTweetableというインタフェースを作った時に、BigIntegerクラスにどの様にtweetメソッドを実装したら良いでしょうか?

一つは、ラッパークラスを作るというのが手になります。WrappedBigInt extends BigInteger with Showable のようなラッパークラスを作るということです。しかし、ラッパークラスへの出し入れの手間やオーバーヘッドを考えたらなるべく避けたいところではあります。それに、一々、implements Showable, Tweetableとtrait(Javaならinterface)を追加していくのも限度があります。演算が100個あったら100個のインタフェースをラッパークラスに実装しなければならないのでしょうか?しんどいですね。ではtraitごとにラッパクラスを分けたらどうでしょうか?ShowableBigInteger、TweetableBigIntegerとか。これもダメですね。showsとtweetの両方を同じコンテキストで使いたくなったときにラッパを二つ作って管理するのは考えただけでしんどいです。

また、継承関係によるインタフェースの追加は、スーパークラスが持つすべてのインタフェース(trait)を実装しなければならないという点もなかなかしんどい点ではあります。クラスの包含関係の中に例外は許されません。

ここで、全く別のアイディアとして、拡張関数を使うのはどうでしょうか。拡張関数ならばアドホックにshowsやtweetを実装できそうです。たとえば、C#でPointクラスにshowsやtweetといった関数をアドホックに追加するには以下のようにします。

public static string shows(this Point p){ ... }
public static string tweet(this Point p){ ... }

こうするとアドホックに関数を追加できます。ラッパクラスも要りません。良かったじゃん。ハッピーハッピー。…となはりません。肝心の多相性が実現できていません。たとえば、Showで生成した文字列の文字列長を求めるような関数を定義したい場合、拡張関数では実装することができません。つまり、

def showLength[T](v: T) = {
  v.shows.length
}

のような書き方が出来ないのです。拡張関数ではTがshowsを実行可能であるような型であるということをコンパイラに教えてあげることができません。「ダックタイピング(C#ではdynamic)を使えば出来るよ」と思われるかもしれませんが、すると型安全性が失われます。実行して見なければエラーが分からないコードになってしまって、これもまたよろしくありません。

PointやBigIntegerといったクラスの外で、アドホックにポリモーフィズムを実現する(この性質をアドホック多相性と言います)にはどうしたらよいでしょうか。関数型プログラミングの世界では、このような問題を型クラスという構造を使って解決します。継承関係で表現するポリモーフィズムとは違ったアプローチです。

以下が型クラスの例です。インタフェースの概念にすこし似ています。

trait Show[A] { def shows(a: A): String }

Pointにshowsメソッドを追加するには以下のようにします。

val showPoint = new Show[Point] { def shows(a: Point) = s"(${a.x}, ${a.y})" }

使うにはこうします。

showPoint.shows(Point(2, 3)) // -> (2, 3)

「え…使う前に実装する必要があるの?それじゃJavaでComparatorを無名クラスで実装するのと大差ないんじゃ…」と思われるかもしれません。たぶん、本質的にはほぼ同じです。「型クラスはComparatorのようなもの」と覚えてしまっても問題ないでしょう。ただ、Scalaの場合はJavaよりもオシャレな書き方にすることは出来ます。

たとえば、以下のようなobjectを作り、インポートしておきます。

object ShowInstances {
  implicit val showPoint = new Show[Point] { def shows(a: Point) = s"(${a.x}, ${a.y})" }
  implicit val showBigInt = new Show[BigInteger] { def shows(a: BigInteger) = a.toString }
}

すると暗黙的に型クラスのインスタンスを取ってこれるようになります。

implicitly[Show[Point]].shows(new Point(2, 3))

これで使うたびに実装するという事を避けることが出来ます。しかしこれでもまだ不満が残ります。implicitlyって単語、長いしlとかiとか似たような文字が並んでるし、日本人には覚えられないよぉ…。というか、他の場所で宣言した値を参照しているのと大差ないよぉ…。と思う人も多いでしょう。ご安心ください。Scalaには値クラスがあります。値クラスを使えば拡張メソッドを実現できます。

implicit class RichPoint(val self: Point) extends AnyVal {
  def shows: String = implicitly[Show[Point]].shows(self)
}

それ、結局ラッパークラスでは?とは思わないでください。値クラスはラッパークラスに見えますが実行時のデータ構造はラップする型のまま処理されます。つまり、ラッパークラス分のメモリは割り当てられず、オーバーヘッドもありません。

結局、これらの構造を使うと以下のように書くことが出来ます。

Point(1, 3).shows // -> "(1, 3)"を出力

一方で、ただの拡張メソッドでは実現できなかったshowLengthメソッドはどのように表現できるでしょうか?以下のようにしておいて、呼び出し時のコンテキストで暗黙的に型クラスのインスタンスを与えてあげればいいわけですね。

def showLength[T](v: T)(implicit show: Show[T]): Int = {
  show.shows(v).length
}

もしくは、Context boundという機能を使います。これは上記と同等のことを実現するシンタックスシュガーです。どちらにしても、関数を呼び出すときにShow[T]のインスタンスが暗黙的に解決されないとエラーになることに注意してください。

def shows[T : Show](a : T) = {
  implicitly[Show[T]].shows(a)
}

はい、できました。おめでとうございます。

「ここまでやってtoStringと同じことを実装したにすぎないのか…」と思ったならば、もう一度アドホック多相性の目的を思い出してください。我々は、元々あったクラスには一切手を加えず、外から新しいインタフェースを定義できたのです。言い換えると、あらゆるクラスに対してtoStringのような共通のインタフェースを外部から定義することが出来たのです。toStringのようにあらゆる値に対して統一的に実行可能なインタフェースを外から定義することが可能になったのです。これは従来の継承の仕組みをベースとしたオブジェクト指向では実現できないことでした。

ちなみに、型クラスで大事なのは、型クラスのインスタンスを作るところのみで、その他の部分は決まりきった書き方を繰り返すことになってしまいます。これを簡素化するための仕組みがCatsには備わっているっぽいので、後々チャレンジしてみたいと思います。

あと、最後にひとつ補足を。

これまで、上記で述べたような「Comparatorのようなクラス」のことを「型クラス」と表現してきましたが、厳密にはこれは限りなく誤りに近い、誤解を招く表現です。正しくは、「型クラスパターン」と呼ぶべきでしょう。普通、「型クラス」というと、関数型言語や関数型プログラミングを実現するライブラリで使用される、ShowsやMonadといった具体的な型クラスを意味します。ちなみにComparatorに当たる、大小関係を定義する型クラスはHaskellではOrd、ScalazではOrderという型クラスとして存在しています。本記事で述べたようなTweetableという型クラスパターンをとるクラスを作成し、それを「型クラス」と呼んでも間違いでは無いかもしれませんが、普通は言わないと思います。