1.概略
Haskellは型にうるさい言語である。型が合っていないと計算してくれない。型が合っているかどうかの検査はコンパイル時に行われるので、厳しい型の言語に慣れていないプログラマにとって、コンパイルを無事通り抜けられるかは大仕事である。しかし、コンパイルを通り抜けてしまえば、プログラムは予想した通り動くので、通常のプログラミングでたくさんの時間をかけるデバッグをほとんど必要としないというのがHaskellの強みである。
それでは、Haskellの型を見ていくことにする。型には二通りある。「型」と「型クラス」である。オブジェクト指向でのインスタンスとクラスの関係と似ているところがあって、型は型クラスのインスタンスとなる。但し、型は、いくつもの型クラスのインスタンスになれる可能性を持っていて、どの型クラスのインスタンスであるかは、プログラムを記述したときに定まる。このような性質、即ち、「インスタンスがいくつかのクラスになれる可能性を有していてプログラムを記述したときにそのクラスが決まる性質」を「多相型」と呼んでいる。例えれば、カメレオンみたいで、その環境に適合するように色を変えられるという性質である。
まず、型から説明する。型には、Int,Integer,Float,Double,Char,Bool,Ordering,tuple,()などがある。Intは有界な整数で、Integerは有界でない整数である。Floatは単精度浮動小数点数で、Doubleは倍精度浮動小数点数である。Charは文字、Boolは真理値型である。Orderingは大小関係を表すEQ,LT,GTである。tupleは( , , .)で表された型であり、空のタプリ()も独立した一つの型である。
次に、型クラスを説明する。Haskellが用意する型クラスには下図のようなものがある。
この図にはクラスの階層も示してある。矢印の上側にある型クラスが上位、下側にある型クラスが下位である。Haskellでは計算をする時に型クラスが合っていないと計算ができない。型を合わせるために、上位の型クラスにあるものを下位の型クラスに移すことが可能である。
また、上位のクラスで定義された関数は、下位のクラスに継承され利用できる。
型クラスは、先に述べた型を自身のインスタンスとすることができるが、許されるインスタンスも図には示した。例えば、Integralという型クラスはIntとIntegerをインスタンスとすることができるが、FloatやDoubleをインスタンスとすることはできない。逆に、Floatingという型クラスはFloatとDoubleをインスタンスとすることができるが、IntやIntegerをインスタンスとすることはできない。逆に、型のほうから見るとたくさんの型クラスのインスタンスとなりえることがわかる。例えば、Intという型は、Eq, Ord, Num, Real, Integralなどのインスタンスとなりえる。
代表的な型クラスについて説明する。Eqは等値性の概念を有するものである。また、関数として、==,/=を用意している。Ordは大小関係の概念を有するものである。また、関数として、>,<,>=,<=,compareを用意している。なお、compareは二つを比較し、大小関係を表すEQ,LT,GTを返す。
Integralは整数を表すものである。その関数としてfromIntegralなどを有する。これは、Integralの型クラスに属してしまった数をNumの型クラスの属すように変更するものである。Haskellでは型クラスが合わないと計算できないので、型クラスが合わないもの同士の場合には、例えば、IntegralとFloatingに属す数を加えるなどの場合、型クラスを変更して同じ型クラスに属すようにする必要がある。fromIntegralはそのための関数で、Integralの型クラスからNumの型クラスに変更し、クラス階層図でNumより下にある型クラスに移動できるようにする。
Floatingは浮動小数点数を扱うもので、その関数として、sin,cos,sqrtなどを有する。
言葉だけでは、理解が難しいので、実際に見ていくこととする。Haskellでは、プログラムでどのような型クラス、型が利用されているかを見ることができる。例えば、加算(+)は次のようにしてみることができる。二つのコロン符号のある行の記述を型シグネチャ(Type Signature)という。
Prelude> :t (+) (+) :: Num a => a -> a -> a
ここで、Num aは、aが型クラスNumに属すことを示している。a -> a -> aは、最初の二つが入力で、最後の一つが出力である。ここでは、入力される値も出力される値もa、即ち、Numに属しているといっている。
値を入れたもので調べてみる。
Prelude> :t 4.5 + 3 4.5 + 3 :: Fractional a => a
今度は、Numではなく、出力aはFractionalと言っている。これは、4.5がFractionalとみなされたためである。この結果、3もFractionalとみなされたはずである。
間接的だが確認する。二項演算子の足し算は、その記号の前あるいは後に数値を置くことによって単項演算子に変えることができる。
:t (4.5+) (4.5+) :: Fractional a => a -> a
これより、入力も出力もFractionalであることがわかる。誤って、文字を加えようとすると次のようにエラーとなる。FractionalとCharの組み合わせたダメだといって
いる。
Prelude> :t (4.5+) 'a' <interactive>:1:2: No instance for (Fractional Char) arising from the literal ‘4.5’ In the first argument of ‘(+)’, namely ‘4.5’ In the expression: 4.5 + In the expression: (4.5 +) 'a' <interactive>:1:5: No instance for (Num Char) arising from a use of ‘+’ In the expression: 4.5 + In the expression: (4.5 +) 'a'
次はリストを作る演算子(:)についてみることにする。この演算子の型シグネチャは次のようになっている。
Prelude> :t (:) (:) :: a -> [a] -> [a]
なんとaには型クラスが指定されていない。どのような型クラスでもよいということになる。但し、入力のaも [a] も出力の [a] も同じ型タイプが要求されている。
ところで、型シグネチャのところで用いているaは、プログラムの記述の中で型クラスが決まるので、型変数と呼ばれ、小文字で始まる変数名である。
ここでも同じように、リストに要素を入れたときの、型シグネチャがどのように変化するかを見る。まず、先頭に文字を入れると次のようになる。
Prelude> :t 'a':[] 'a':[] :: [Char]
型クラスがCharに変化する。先頭に数字を入れると次のようになる。
Prelude> :t 3:[] 3:[] :: Num a => [a]
型クラスがNumに変化する。4.0を先頭の数字に用いると
Prelude> :t 4.0:[] 4.0:[] :: Fractional a => [a]
型クラスがFractionalに変化する。3/4としても同じである。
Prelude> :t 3/4:[] 3/4:[] :: Fractional a => [a]
sqrtを用いるとFloatingになる。
Prelude> :t sqrt 2 : [] sqrt 2 : [] :: Floating a => [a]
2.例題
分数とsqrtを一緒にした時の型シグネチャはどうなるか。
分数は型クラスがFractional、sqrt型クラスがFloatingである。型が一緒でないので、下位のクラスに移して型が合うようにする。分数はFloatingにもなれるので、一緒に用いたときの型クラスはFloatingと考えられる。実際次のようになっている。
Prelude> :t sqrt 2 : [] sqrt 2 : [] :: Floating a => [a]
2.問題
それでは以下の型シグネチャを求めてみよう
問題1:リストの長さを求める関数length
問題2:正弦関数sin
問題3:左側のほうが大きいかを示す関数(>)
問題4:4より小さいかを示す関数(4>)
問題5:4.5より小さいかを示す関数(4.5>)
問題6:sqrt 2 より小さいかを示す関数 (sqrt 2>)
問題7:’a’ より小さいかを示す関数 (‘a’>)
問題8:”bird”より小さいかを示す関数 (“birs”>)
問題9:先頭からの部分リストを取り出す関数take
問題10:無名関数 \ x y -> x * x + y * y