趣味でScalaを使っていますが高校で数学に挫折した私にはScalaとかHaskellの型クラスの説明がよくわかりません。なので数学を使わずに説明してみようかと思います。数学的に間違っていたら指摘してもらえれば訂正します。
数学を使わずに何を使うかというと数学以前のもの、つまり古代ギリシア数学=ユークリッド幾何学を使います。高校一年生の数学つまりこれから数学を始める人用程度の難しさなので数学の分からない私でもギリ分かります。
ユークリッド幾何学のルール※読み飛ばし可
1. まず点や線などの基礎的な概念に対する定義を与える
2. 次に一連の公理・公準を述べ、公理系を確立する
3. 対象となる図形を想定し、1と2といくつかの点から図形を描く
4. また新たな対象となる図形を想定し、1と2と3を使い新たな図形を描くことを繰り返す
要は、単純なルールを組み合わせて図形を描く、一度作図した図形は以後使うことができる、逆に言えば何かを描きたいときは必ずその操作を記述しなければいけない、という事です。
例えば、直線を半径として円を描くことができ、この円の直径は半径の倍になります。この操作を一度やれば、次からは単に「直線を倍に伸ばす」操作を行っていいという事ですね。
例えば、私が正三角形の外側にぴったりはまる円(外接円)を描きたいとします
日曜大工で木工をするときにはちょくちょく木に三つ目錐で穴をあけます。
三つ目錐は正三角形をしており、くるくる回すことでその外側にぴったりはまる円(外接円)となる穴が開きます。だから正三角形から外接円が作図できれば、錐の大きさから穴の大きさが計算できます!
正三角形は直線=一片の長さが分かれば簡単に書けます。直線の両端で円を描いて交点と両端を結べば正三角形です。
class 正三角形(一片の長さ : int) { /* 一片の長さの直線を引いて両端で円を描いて交点と両端を結ぶ */}
一度作図した操作は使うことができます。
val 一片の長さ3の正三角形 = 正三角形(3/*一片の長さ*/)
では正三角形の外接円は描けるでしょうか?答えはもちろん「まだ描いてないから分からない」です。
val 一片の長さ3の外接円 = 正三角形(3).外接円を描く() //外接円の描き方がまだわからないからコンパイルエラー
正三角形でなくても外接円は描けるかもしれません
ところで錐には四角の四ツ目錐もあります。
ということは四ツ目錐の穴の大きさが必要になったら、正方形の外接円の作図方法も必要になるわけですね。ひょっとして他の図形にも外接円が描けるのでしょうか?
この、例えばなんかの図形に外接円が描けるかもしれない、というのが型クラス(の見込み)です。(見込み)ってのは「あるかどうかもわからないもの」の正しい名前を私が知らないだけなので数学史的に根拠のある指摘をいただけたら修正します。
実のところ正三角形に外接円が描けそうというのすら順番がおかしくて、より正確には
仮にある図形に対して外接円が描けるだろうか?描けるとして、正三角形に描けるだろうか?ひし形には?正方形には?ひょっとして少なくとも任意の正N角形には描けるのだろうか?というのが古代ギリシア数学的な考え方です。
正三角形の外接円も正方形の外接円もありそうな気はしますが気がするだけじゃ有る証明にはならないのでその具体的な描き方を示す形で論理を展開します。
型クラス(の見込み)はScalaではtraitを使って書きます。次のコードは「任意の図形Aに対して外接円が描けるかもしれない。もしその図形に対して外接円を描く作図法を具体的に記述したら外接円が描けるってことにしよう」という意味になります。
trait 外接円が描ける[A]{
def 外接円を描く():Circle
}
操作は複数書くことができます。例えば、外接円に更に外接する正方形を作る操作とか。錐で穴をあけた後に4隅を削って四角い穴を作るとかそんなときには書き足すかもしれません。
trait 外接円が描ける[A] {
def 外接円を描く() : Circle
def 外接円に外接する正方形() : 正方形
}
つまり型クラス(の見込み)というのはある図形に対して追加する操作のまとまり、ということですね。古代ギリシア数学では操作というのは図形に対してどんどん追加していくものでした。
正三角形に対する外接円を描くコードを正三角形にバインドしたもの、が見込みではなく具体的になった型クラスになります。implicitで正三角形にバインドできるので書いてみましょう。
型はメソッドを持たないしtypeという名前で宣言したいところですがScalaなのでclassです。
// 型クラス(の見込み)。
trait 外接円が描ける[A] {
def 外接円を描く() : Circle
def 外接円に外接する正方形() : 正方形
}
//円は半径が解ってれば描ける
case class Circle(半径 : Int){}
//正三角形は一片の長さが分かれば描ける
case class 正三角形(一片の長さ : Int) {}
//正方形は一片の長さが分かれば描ける
case class 正方形(一片の長さ : Int) {}
object 外接円が描ける {
val 一片の長さ3の正三角形 = 正三角形(3)
val 一片の長さ3の正三角形の外接円 = 正三角形(3).外接円を描く()
// (that:正三角形)に対して「正三角形は外接円が描ける」という名前の型クラスを追加する。
// これは「任意の図形(今回は正三角形)Aに対して外接円が描ける」見込みを満たす。
implicit def 正三角形は外接円が描ける(that:正三角形) : 外接円が描ける[正三角形] = new 外接円が描ける[正三角形] {
override def 外接円を描く() : Circle = Circle(that.一片の長さ/*から計算できるけど省略*/)
override def 外接円に外接する正方形() : 正方形 = 正方形(that.一片の長さ/*から計算できるけど省略*/)
}
}
これで、正三角形に外接円を描く操作が追加されました。
開発環境にコピペして implicit から始まるブロックをコメントアウトしてみましょう。外接円を描くメソッドがコンパイルエラーになるのが分かるはずです。
Scalaのコンパイラは呼び出しのタイミングにおいて宣言済みと認識できるどこかに具体的な描き方が書いてあるか探します。(古代ギリシア数学的には物理的に前の項にあるべきですがコンパイラは記述内容が前後していなければ記述位置の前後を気にせず解釈してくれます)
具体的な描き方が書かれていなければ描き方が分からないのでコンパイルエラーになりますが書いてバインドした後は描き方が分かっているので描ける=コンパイルできます。
これで「外接円が描けることが記述済みの正三角形」型クラスができました。
これで、例えば「外接円が描ける図形を受け取って実際に外接円を描く操作」、つまり関数に正三角形や正方形を渡して外接円を描かせることとかができるようになりますがここから先は普通の型クラス説明記事を読んだほうが分かりやすいかと思います。
実は型クラスの考え方はクラスより古い
このように、操作=型クラス(の見込み)を中心として想定しそれを実現する型クラスを追加するのを繰り返すことによって古代ギリシア数学は成立しています。「見込み」の段階では型の想定すらしていませんし実現する型クラスは存在しなかったってこともあり得ます。
一方、クラスはある型を中心にしてそれの持つ操作をまとめるという思想で、ここまでクラスが登場してないように実はクラスは必要ありません。
ですがそこに登場するのがプラトンです。
プラトンは言いました。「お前ら数学者が操作を追加しなくても正三角形は外接円を描けるものであるはずだ。外接円の描き方を作ったとかおこがましい。"正三角形は外接円を描けるものであり、私が発見した描き方はこうだ"という言い方をするべきだし、型に対して操作をまとめるべきだ。」
これがのちにクラスとなるイデアです。
つまりクラスは型クラスを型中心にしてそれに対する操作という形にまとめ直したものです。
この見方は当時「正三角形とはどういうものか、つまり型を中心にして考えるならそうだよね」くらいの受け取られ方をしていたらしく、数学上ではそんなに使われていなかったそうです。現にプラトンの後の時代であるユークリッドの原論でも操作中心の描き方をしています。
ですがプラトンの思想が教会に受け継がれたことと、近代哲学がプラトンを再発見したときに「プラトンすげえ!」となったことによりクラス中心の思考法が中心になったそうです。
更に運の悪いことに、プラトンやその弟子であるアリストテレスがイデアやオブジェクトなどの概念にバンバン名前を付けた一方で型クラスに名前を付ける人はいませんでした。型クラスは当時当たり前の考え方だったので名前を付ける必要すらない空気のようなものだったのかもしれません。
プラトンが型クラスを嫌わずに適切な名前を付けていれば現代になって型クラスなんてクラスのパチモンみたいな名前になることはなかったのかもしれませんね。