やさしい Haskell 入門 ( バージョン 98 )
back
next
top
最後に他のプログラミング言語とは違う Haskell の型システムの特徴について 説明しましょう。すでに述べてきた多相は、パラメータ ( parametric )多相とよばれるものです。このほかに アドホック多相とよばれる種類のものもあります。これは 多重定義( overloading )という呼びかたのほうがよく知られ ているかもしれません。アドホック多相の例をいくつかあげると、
1、2 などのリテラルは、固定長の整数と任意長の整数の両 方でつかわれることが多い。
+ のような数値演算子は何種類もの数値上で定義されることが多い。
同値演算子( Haskell では == )はふつう数値とその他多く(すべてで はない)の型の上で動作することが多い。
単純ですが、重要な例、同値性の例からはじめましょう。同値性を定義したい型
には多くの型があります。しかし、いくつかの型については同値性を定義したく
ないこともあります。たとえば、一般的にいって、関数が同値であるかどうかの
判定は計算では処理が困難です。その一方で、ふたつのリストが同じかどうかを比
較したいことはよくあることです。(ここで、同値性といっているのは、「値同値
性」のことです。対照的な概念としては、「ポインタ同値性」というのがありま
す。たとえば、Java 言語の == です。ポインタ同値性は参照透明性を
持ちません。それゆえに純粋な関数型言語とは相性がよくありません。) このこ
とをもっと明かにするために、リストの要素かどうかを検証する elem
という関数の定義をみてみましょう。
x `elem` [] = False
x `elem` (y:ys) = x==y || (x `elem` ys)
[ 3.1 節で議論した構文的な理由に
より、elem を定義するのに中置形式を選択します。== と
|| は、それぞれ、同値演算子と論理和演算子です。]
直観的にいえば、elem の型は a->[a]->Bool 「でな ければなりません。」 しかし、そのためには == の型は a->a->Bool でなければなりません。さきほど、全ての型につい て == を定義したいわけではないと言ったばかりにもかかわらずです。
さらに、こんなことにも注目しています。もし、 == を全ての型の上 で定義できたとしても、ふたつのリストを比較することと、ふたつの数を比較す ることは全然違うことです。こういう意味で、これらいろいろな仕事をさせるた めに、== が多重定義できることを期待するわけです。
型クラス( type class )はこのふたつの問題を解決するのに都
合のよい仕組です。型クラスを使うと型をいずれかのクラスのインスタンスとし
て定義できます。また、クラスに付随した多重定義された操作の定義
をおこなうことができます。たとえば、同値演算子を含むクラスの定義をしてみ
ましょう。
class Eq a where
(==) :: a -> a -> Bool
ここでは、Eq は定義するクラスの名前です。また == はこ
のクラスでの唯一の操作です。この宣言は、「型 a がクラス
Eq のインスタンスであるのは、(多重定義された) == が存
在し、そのうえで定義された適切な型をもつ場合である」と読むことができます。
(このとき、== は同じ型の対象の組の上でのみ定義されることに注意
してください。)
型 a がクラス Eq のインスタンスでなければならない、と
いう制約は、 Eq a のように書きます。ということは、
Eq a は型式ではなく、型に対する制約を表現していますので、
文脈( context )と呼ばれます。文脈は型式の前に置きます。
たとえば、上のクラス宣言の効果は、次のような型を == に割当るこ
とになります。
(==) :: (Eq a) => a -> a -> Bool
これは、「クラス Eq のインスタンスであるような型 a の
それぞれに対して、== は a->a->Bool という型をもつ」
と読むことができます。これは、まさに elem の例で使われている
== の型です。実際にも、この文脈による制約が elem の主
型に波及します。
elem :: (Eq a) => a -> [a] -> Bool
これは、「クラス Eq のインスタンスであるような型 a の
それぞれに対して、elem は a->[a]->Bool という型
をもつ」と読むべきです。これこそ、求めていたものに他なりません。これは、
elem がすべての型の上で定義されているわけではなく、その要素につ
いて同値性の比較方法がわかっている型の上で定義されているということを示し
ています。
ここまではいいでしょう。しかし、どのようにして、どの型が Eq ク
ラスのインスタンスになるかを指定し、その型に対する == の実際の
振舞いを指定するのでしょうか。これは、インスタンス宣言
( instance declaration )でおこないます。たとえば、
instance Eq Integer where
x == y = x `integerEq` y
のようにします。== の定義はメソッド( method )と
呼びます。integerEq 関数は、この場合、たまたま、整数の同値性を
比較するプリミティブ関数ですが、一般には、右辺では適正なものであれば、ど
のような式でもよいことになっています。これは、一般の関数の定義と同じです。
この宣言全体は、「 Integer 型はクラス Eq のインスタン
スで、ここに、操作 == に対応するメソッドを定義する。」 と読むこ
とができます。この宣言をすれば、任意倍長の整数の同値性を == を
用いて比較することができます。同様にして、
instance Eq Float where
x == y = x `floatEq` y
により浮動小数の比較を == を用いておこなうことができます。
前に定義した Tree のような再帰的な型でも同じよう扱うことができ
ます。
instance (Eq a) => Eq (Tree a) where
Leaf a == Leaf b = a == b
(Branch l1 r1) == (Branch l2 r2) = (l1==l2) && (r1==r2)
_ == _ = False
最初の行の Eq a という文脈は必須です。なぜなら、
葉のもつ要素(型は a )は 2 番目の行の同値性を用いて比較します。
付加される制約は本質的には、a の木の同値性の比較は、すでにその
やりかたが知れている a の同値性比較を用いて実現できるということ
です。もし、文脈をインスタンス宣言から取り除くと、静的型エラーが発生しま
す。
Haskell レポート、特にプレリュードには便利な型クラスの例が豊富に含まれて
います。実際には、Eq クラスの定義は前に定義したものよりは、もう
すこし規模がおおきいものです。
class Eq a where
(==), (/=) :: a -> a -> Bool
x /= y = not (x == y)
これは、ふたつの操作子をもつクラスの例です。ひとつは同値性の操作子、もう
ひとつは、非同値性の操作子です。この例は、また、
デフォルトメソッドの使い方の例示になっています。この場合は非同
値性操作 /= がそれです。ある特定の操作に対するメソッドがインス
タンス宣言に含まれていないければ、そのクラス宣言中に定義されているデフォ
ルトメソッドがあれば、それが使われます。たとえば、まえに定義した 3 つの
Eq クラスのインスタンスは上のクラス宣言で完全に動作します。この
とき、望む非同値性の正しい定義(同値性の論理否定)がつくりだされています。
Haskell ではクラス拡張( class extension )の記法もサポー
トされています。たとえば、Eq クラスからすべての操作を
継承( inherit )した Ord クラスを定義したいとし
ましょう。このクラスは Eq クラスから継承した操作のほかに、比較
操作子と min 関数および max 関数をもつものとします。
class (Eq a) => Ord a where
(<), (<=), (>=), (>) :: a -> a -> Bool
max, min :: a -> a -> a
このクラス宣言の文脈に注目してください。Eq は Ord の
スーパークラス( super class )であるといいます。(反対に、
Ord は Eq のサブクラス( subclass )であ
るといいます。) Ord のインスタンスである型はすべて Eq
のインスタンスでなければなりません。(次節では Ord の完全な定義
をプレリュードから抜きだし紹介します。)
このようなクラスの包含の利点のひとつは、文脈の簡略化にあります。
Eq と Ord の両方のクラスの操作を使用する関数に対する型
式は文脈 (Eq a, Ord a) ではなくて
(Ord a) を利用することができます。それは、Ord が
Eq を包含しているからです。さらに重要なことは、サブクラスの操作
に対するメソッドは、スーパークラスの操作に対するメソッドが存在することを
仮定することができるということです。たとえば、プレリュード中の
Ord の宣言は、(<) に対応するデフォルトメソッドを含
んでいます。
x < y = x <= y && x /= y
Ord の使用方法の一例として、2.4.1 節で定義した quicksort
の主型の型付を見ると次のようになります。
quicksort :: (Ord a) => [a] -> [a]
いいかえれば、quicksort は順序付の型の値のリスト上でのみ操作で
きるということです。この quicksort に対する型付は定義のなかの比
較操作子< と >= の使い方から導かれます。
Haskell では多重継承( multiple inheritance )も認められて
います。それはクラスは 2 つ以上のスーパークラスを持つこともあるからです。
たとえば、次の宣言をみてみましょう。
class (Eq a, Show a) => C a where ...
これは、C というクラスをつくっていますが、操作を Eq と
Show の両方から継承しています。
Haskell ではクラスメソッドはトップレベルの宣言として扱われます。これらは、 ふつうの変数と同様に同じ名前空間を共有します。名前はクラスメソッドと変数 名あるいは別のクラスのメソッドとの両方を表示するのには使えません。
文脈はデータ宣言のなかで使うことができます。これについては §4.2.1 を参照して ください。
クラスメソッドには、あらゆる型変数(定義しようとしているものを除く)の上の
制約を付加することができます。たとえば、
class C a where
m :: Show b => a -> b
このようなクラスでは、メソッド m は型 b が
Show クラスに属していることを要求します。しかしながら、メソッド
m は型 a については、いかなるクラス制約も付加しません。
これらは、クラス宣言のなかの文脈でおこなう必要があります。
ここまで、「1階の」型を使ってきました。つまり、型構築子 Tree はこれまで常にひとつの引数と組になっていました。たとえば、 Tree Integer ( Integer 型の値を含む木)、あるいは、 Tree a ( a 型の値を含む木の族をあらわす)です。し かし、Tree それ自身は型構築子であり、ひとつの型を引数としてとり、 ひとつの型を返します。Haskell にはこの種の値はありませんが、このような 「高階の」型はクラス宣言のなかで使用することができます。
まずはじめに、以下のような Functor クラス (これはプレリュードか
らの引用) を考えましょう。
class Functor f where
fmap :: (a -> b) -> f a -> f b
fmap 関数は以前につかった map 関数を一般化したものです。
型変数 f は f a のなかで他の型に適用されます。こ
のように、型変数が Tree のような引数をとる型に束縛されることが
期待されます。型 Tree に対する Functor のインスタンス
は、
instance Functor Tree where
fmap f (Leaf x) = Leaf (f x)
fmap f (Branch t1 t2) = Branch (fmap f t1) (fmap f t2)
となります。このインスタンス宣言は Tree a ではなく
Tree が Functor のインスタンスであることを宣言していま
す。こうした能力は大変便利なものです。これは総称的「コンテナ」型を記述す
る能力のあることを示し、fmap のような、任意の木、リストあるいは
他の型の上で統一的に動作する関数を可能にします。
[型適用は関数適用とおなじやりかたで書きます。型 T a b は (T a) b のように構文解析されます。タプルのような特 別な構文を使う型はカリー化可能な別のスタイルで書きます。関数については、 (->) が型構築子です。f -> g と (->) f g とは同じです。同様に、[a] と [] a とは同じです。タプルについては、型構築子(同時にデー タ構築子でもありますが)は (,)、(,,) などとなります。]
すでにご存知のように、型システムは式の中で型付けエラーを検出します。しか し、不正な型式に起因するエラーについてはどうでしょうか。式 (+) 1 2 3 は型エラーになります。それは、 (+) は 2 つしか引数をとらないからです。同様に、 Tree Int Int は、ある種のエラーを起こすはずです。それ は、Tree 型はひとつの引数しかとらないからです。では、Haskell は どのようにして不正な型式を検出するのでしょうか。その答は、型の正当性を確 認する第 2 の型システムにあります。それぞれの型は類 ( kind )と結びついています。これはその型が正しく使用されることを 確認するものです。
型式は別々の類に分類されます。これは次の 2 つの形式をとります。
記号 * は具体的なデータ対象に結びついた型の類をあらわす。すなわち、 もし、値 v の型が t であるなら、v の類は * で なければならない。
もし、k1 と k2 とが類ならば、k1->k2 は k1 という類の型をとり、k2 という類の型を返す型の類である。
類は Haskell のプログラムの直接あらわれることはありません。コンパイラが 型検査の前に類を推論します。「類宣言」のようなものは必要ありません。型シ グネチャが類エラーをおこすとき以外は、類は Haskell のプログラムの後ろに 隠れています。類は非常にシンプルなので、コンパイラは、類衝突がおこれば、 その旨のエラーメッセージを表示することができなければなりません。類に関す る詳細は §4.1.1 や §4.6 を参照してく ださい。
型クラスの使い方のさらに進んだ例にいくまえに、Haskell の型クラスのふたつ の別の観点について言及してもいいかと思います。第一は、オブジェクト指向プ ログラミング( OOP )とのアナロジーによる観点です。OOP に関してよくある次 のような謂は、単純にクラスを型クラスへ置き換え、オブジェクトを 型へ置き換えれば、ほぼ Haskell の型クラス機構になります。
「クラスは操作の共通集合を捕捉するものである。ある特定 のオブジェクトはクラスのインスタンスであり、おのおのの操作に対 応するメソッドをもつ。クラスは階層構造をもつことがあり、スー パークラスやサブクラスという概念があり、操作/メソッドの継 承も許されている。デフォルトメソッドがある操作と結びつくことも ある。」
OOP と対比すると、型はオブジェクトではないというのは明かで、特 に、オブジェクトや型の可変の内部状態などという概念はありません。いくつか の OOP 言語に対する利点は、Haskell のメソッドは完全に型安全であるという ことです。必要なクラスのなかにはない型の値にメソッドを適用しようとしてい るかどうかは、実行時ではなくコンパイル時に検出されます。つまり、メソッド が実行時に「検索」されるのではなく、単純に高階関数として引き渡されるだけ です。
パラメータ多相とアドホック多相と関係を考察すると別の見方をすることができ ます。すべての型の上で全称修飾することで型の族を定義するさいに、パラメー タ多相がどれほど便利かということは既に示しました。しかしながら、ときには、 このような全称修飾では広すぎることもあります。もっと小さい型の集合の上で 限量修飾をしたい場合があります。たとえば、比較同値性をもつような型の集合 の上で、ということです。型クラスは、これを実現する構造的方法とみなすこと ができます。実際のところは、パラメータ多相は一種の多重定義とみなすことが できるのです。まさに、多重定義は暗黙にすべての型の上で起こるのであって、 限定された型の集合(つまり、型クラス)上で起こるのではありません。
Haskell で用いられるクラスは C++ や Java のようなオブジェクト指向言語で 用いられるクラスに類似しています。しかし、いくつかの重大な相違があります。
Haskell は、型の定義とその型に結びついているメソッドの定義を分離す る。 C++ や Java のクラスは、ふつう、データ構造(メンバー変数)とその データ構造と結びついた関数(メソッド)との両方を定義する。Haskell ではこれらの定義は別々におこなう。
Haskell のクラスにより定義されたクラスメソッドは、C++ のクラスにお ける仮想関数に対応している。クラスのインスタンスのそれぞれは、各メ ソッドについてそのインスタンス自身の定義を与えます。デフォルトメソッ ドはベースクラスの仮想関数のデフォルト定義に対応している。
Haskell のクラスは大筋では Java のインタフェースに類似している。 Java のインタフェース宣言とおなじように、Haskell のクラス宣言はオブ ジェクトそのものを定義するのではなく、オブジェクトを使う手順を定義 している。
Haskell は、型の違う関数が共通の名前を共有するような C++ 流の多重定義 はサポートしていない。
Haskell のオブジェクトの型は暗黙の型変換を受けない。値が投入あるい は投出される Object のようなすべての基になるようなクラスは ない。
C++ や Java ではオブジェクトの実行時表現に( VTable のような)同定情 報を付加する。Haskell ではこのような情報は、型システムを通じて物理 的にではなく、論理的に、値に付加する。
Haskell のクラスシステムには、( public あるいは private などといっ たクラス構成要素のような)アクセスコントロールの機構は組込まれていな い。