Haskellでの多態 (多相)

(2009.1.3)

Haskell での polymorphism について。

オブジェクト指向だと polymorphism (ポリモーフィズム) は「多態」と訳しますが、Haskell 界 (?) では「多相」というみたいです。

オブジェクト指向言語では、あるオブジェクトに対するメソッド呼び出しが、そのオブジェクト(の種類、クラス)によって異なる振る舞いをする、という意味で使われます。

Haskellにはパラメータ多相とアドホック多相があります。

パラメータ多相

Haskell は、静的に (=コンパイル時に) 型チェックしますが、型が違うだけで実装が共通化できることがよくあります。こういうとき、型をパラメータとします。このような多態をパラメータ多相 (parametric polymorphism) といいます。

次の例は、何かのリストの長さ(要素の数)を数えます。a は型変数で、どのような型でも受け付けます。

Haskell
[POPUP]
  1. my_length :: [a] -> Int
  2. my_length [] = 0
  3. my_length (x:xs) = 1 + my_length xs

Java Generics, あるいは C++ の template と同じです。C++のtemplateを使って次のように書けます。イテレータを引数に取ったほうがC++っぽいですが。

C
[POPUP]
  1. template <typename Ty>
  2. int my_length(const Ty& list) {
  3. int count = 0;
  4. typename Ty::const_iterator it;
  5. for (it = list.begin(); it != list.end(); it++)
  6. count++;
  7. return count;
  8. }

アドホック多相

アドホック多相 (ad-hoc polymorphism) の前に、クラスについて解説しておきます。

Haskell にもクラスがあります。ただし、メジャーなオブジェクト指向言語のクラスとはずいぶん様子が違います。

メジャーな、クラスベースのオブジェクト指向言語では、クラスは型です。クラスではない型もあったりしますが、ほとんどクラス = 型と言っても差し支えありません。クラスはオブジェクトのひな形であり、クラスをインスタンス化したものがオブジェクトです。

さらに、クラスを継承して拡張することができます。

Haskell のクラス(型クラス)は型ではありません。それ自体ではオブジェクトを生成できないが、複数の型に共通するメソッドを括りだして定義できるようなものです。

型が型クラスのインスタンスになります。

用語の使い方が違いますが、JavaやC++ の抽象クラスが感覚として近いと思います。

型クラスにもとづき、型によって振る舞いを変える多重定義 (overloading) のことをアドホック多相と呼びます。

Prelude モジュールで、Eq 型クラスが次のように定義されています。また、==, /= 関数が定義されています。Eq 型クラスのインスタンス (=型) を宣言するときに上書き(再定義)しないと、こちらの定義が使われます。

Haskell
[POPUP]
  1. class Eq a where
  2. (==), (/=) :: a -> a -> Bool
  3. -- Minimal complete definition:
  4. -- (==) or (/=)
  5. x /= y = not (x == y)
  6. x == y = not (x /= y)

CharはEqのインスタンスです。以下では (==) だけを定義しています。

Haskell
[POPUP]
  1. instance Eq Char where
  2. c == c' = fromEnum c == fromEnum c'

インスタンス宣言が型宣言の一部ではないないのが重要です。新たに定義したクラスで既存の型に対してインスタンス宣言を書くことができます。

Haskell の型クラスは、二つの効果があります。

  1. 関数をオーバーロード (overload) できる
  2. 引数などの型に制約を加えることができる

例えば、数値を表示するということと文字列を表示するということは全く異なる実装ですが、同じshow 関数でできます。(Show 型クラス)

サンプルを書いてみます。

Haskell
[POPUP]
  1. class Foo a where -- (1)
  2. hoge :: a -> String
  3. hoge x = "default function"
  4. instance Foo Int where -- (2)
  5. hoge _ = "Foo Int function"
  6. instance Foo Char -- (3)
  7. -- f :: (Foo a) => a -> String これはエラー. show が起動できない
  8. f :: (Foo a, Show a) => a -> String -- (4)
  9. f x = hoge x ++ " = " ++ (show x)
  10. main = do
  11. putStrLn $ f (10::Int)
  12. putStrLn $ f 'a'

(1) まず型クラスFooを定義します。hoge関数を持ちます。

(2) 型IntがFooのインスタンスだと宣言します。既存の型でも構わないことに注目。関数をオーバーライドします。

(3) CharもFooのインスタンスとします。こちらは関数をオーバーライドしません。Fooでの定義が使われます。

(4) 関数fを定義します。関数fの引数には、FooとShowのインスタンスを渡さなければならないことを示します。(Foo a, Show a) => の部分が制約を示します。

実行結果は次のようになります。

Foo Int function = 10
default function = 'a'