目次
Haskellにおける型
Haskellでは型という概念は関数と同じぐらい重要なポジションを占めています。
Haskellでは実行する前にすべての値や変数の型がわかっています。このことによって多くのプログラムのバグを実行する前に発見することができるという性質があります。
Haskellを書いていると、型が一致していればだいたい動くのではないか、という錯覚に陥るぐらいにHaskellの型システムは多くのバグを事前に取り除いてくれます。
ghciをつかえばあるものがHaskellでどのような型を持っているのかを調べることができます。:tコマンドに続けて式を入力すれば、それがどんな型なのかを教えてくれます。
::という記号は前にあるものがうしろにある型を持っていることを表す記号です。上の例の場合、"hoge"は型[Char]を持つのだということがわかります。
角括弧はリストを表しているので、文字のリストの型を持っているのだということがわかります。
型を明示的に示す
Haskellには型推論があるので、通常型をプログラム中で示す必要はありません。しかしプログラムの中で型を明示的に示すこともできます。
型を明示的に示すには、先ほど示した::をつかいます。型を示したい対象の後ろに::を書いて、型名を書けばよいのです。
実は関数も型を持ちます。このことは非常に重要なことなので絶対に覚えて帰ってください。関数の型というのは引数の型と戻り値の型で決まります。
たとえば次のような関数を考えましょう。
1 f x = if x `mod` 2 == 0 then "even" else "odd"
この関数fは、xを引数にとってそれが偶数なら"even"を返し、奇数なら"odd"を返します。この関数の型は次のように書けます。 (Haskellをちょっと知っている人なら疑問が出てくるかもしれませんが、ここはそういったことは考えないことにします)
1 f :: Int -> [Char]
気持ちで読んでみましょう。fという関数はIntという型のなにかをとって、[Char]型の何かを返すのだ、という風に読めますね。このように引数の型を書いて、それに続いて->を書き、最後に戻り値の型を書いたものが関数の型になるわけです。
複数の引数がある場合はどうなるでしょうか。2つの整数を足し合わせる関数を考えます。
1 addTwo x y = x + y
この型は次のようになります。
1 addTwo :: Int -> Int -> Int
引数が何個になろうが、->で区切って型を並べるという点は変わらないのですね。なぜ引数と返り値を区別していないのか疑問に思った人もいるかもしれません。
それは後にカリー化という概念をやった時にわかります。
一般にHaskellでは簡単な関数でない限り、関数に明示的に型を与えることが良い習慣だと考えられています。
Haskellの基本的な型
型の名前は必ず大文字から始まります。
Int
Intは整数を表す型です。コンパイラによりますが、Intには上限と下限があります。
Integer
Integerも整数に使いますが、IntegerにはIntのような上限と下限がありません。ただしIntegerの値を処理するのはIntに比べると遅いです。
Float
Floatは小数を表す型です。
Double
Doubleも小数を表す型ですが、Floatに比べて倍の精度を持ちます。
Bool
Boolは真理値型です。TrueとFalseのどちらかの値を持ちます。
Char
Charは文字を表す型です。Charはシングルクォーテーションで囲んで表します。Charのリストは文字列になります。
タプル
タプルも型の一種です。但し前に述べたとおりその要素の型と数によってそれぞれ別の型になります。
空のタプル()はなにか特別な結果を返す必要がない関数の戻り値として使われることがあります。これをUnitと呼びます。
型変数
Haskellの関数を少し調べてみると、いろいろな型に対して動作する関数があることに気づきます。たとえばheadという関数を思い出しましょう。
headはリストの要素が数だろうと、文字だろうと、はたまたリストであろうと動きます。この関数はあらゆる型のリストに対して動作しないといけません。
head関数の型は次のようになっています。
1 head :: [a] -> a
これはaという名前の型があるという意味ではありません。このaを型変数と言って、どんな型でもとりえますよ、という意味を持ちます。
型変数を使うことで、型の利点を殺さぬまま、関数を色々な型のデータに対して使うことができるようになります。
型変数を用いた関数を多相型関数と言います。
型変数にはどんな名前を使っても良いですが、一般にはaやbといった1文字の名前をつけることが多いです。
ペアの前の要素を返す関数fstの型を見てみましょう。
1 fst :: (a, b) -> a
fstは大きさ2のタプルを引数にとって、その1つ目と同じ型の値を返すことが明らかにわかります。
型クラス
型クラスは、型に特定の性質を与えたいときに使うシステムです。
具体的な例を見ながら型クラスというものを考えて行きましょう。型クラスの中で最もよく知られているものの一つがEq型クラスというものです。
Eq型クラスは型に対して「等価性」を定義したいときに使う型クラスです。
たとえばInt型を考えてみましょう。Int型は整数なので明らかに等しいか等しくないかを判定できるはずです。
Char型も考えてみましょう。Char型は文字なのでやっぱり等しいか等しくないかを判定できるはずです。
このようにある型が表しているものが「等価性」というものを定義できる対象であるとわかったなら、その型をEq型クラスのインスタンスにします。
ある型をEq型クラスのインスタンスというものにすることで、その型が表すものは「等価性」というものを判定できる性質を持っているのだということを定義できます。
CharもIntもHaskellの初期状態で、Eq型クラスのインスタンスになっています。
「等価性」というものを判定できる性質を持っていることを定義できると何が嬉しいのでしょうか?
実は「等価性」を判定できる性質を持っていることを定義したら、==演算子をその型に対して必ず定義しなければいけません。
つまりEq型クラスのインスタンスになっていることで
のような比較ができるということが完全に保証されます。逆に言うと、Eq型クラスのインスタンスになっていない型のデータはこのように==を使って等価かどうかを判定できる保証はないのです。
ある型がプログラムで登場した時、それがEq型クラスのインスタンスになっていれば、なんのためらいもなく==を使っても問題ない、ということが保証されます。
インスタンスになっていなければ、==を使うと「そんな関数は使えないよ!」と怒られるかもしれない可能性があるというわけです。
言い換えれば型クラスは、ある型を型クラスのインスタンスにすることで、関数を定義することを強制することができ、それによって型が特定の性質を持つように保証するためのシステムだと言えます。
今の例だと、Int型やChar型はEq型クラスのインスタンスになっているので、Int型やChar型の値を引数にとる==という関数を定義することが強制されています。
それによってIntやCharの値を==を使って等しいかどうか判定できることができる、ということを保証しているわけです。
今説明したことを実際に==の型を見ながら確認しましょう。
1 (==) :: (Eq a) => a -> a -> Bool
(Eq a) => という見たことない表現が出て来ました。これを型クラス制約と言います。これは後ろに出てくるaという型変数はEq型クラスのインスタンスであるような型でないといけないということを示しています。
前に出てきた型変数はなんの制約もなく、どんな型が来てもおkというものでした。
しかし今回は前に(Eq a) =>というものがついています。これによってEq型クラスのインスタンスであればどんなものでもおkという風に若干条件がきつくなっているのです。
以下に代表的な型クラスを挙げます。
Eq
Eqは等価性を調べることができる型にたいして使われる型クラスです。Haskellの標準で存在する型はほぼすべて(関数など特殊なものを除けば)Eq型クラスのインスタンスです。
Eq型クラスのインスタンスにした場合、==と/=という関数を定義しなければいけません。しかし普通は/=は==の逆なので、==だけ定義すれば十分なことが多いです。
Ord
Ordは何らかの順序を付けられる型のための型クラスです。関数を除けばHaskellの標準で存在する型はすべてOrd型クラスのインスタンスです。 Ord型クラスのインスタンスには、<, <=, >, >=という関数を定義しなければいけません。
Show
Showは文字列として表現できる型のための型クラスです。関数を除けばすべてのHaskellの標準で存在する型はShow型クラスのインスタンスです。 Show型クラスのインスタンスに定義されている関数で、最もよく使うのがshow関数で、与えられた引数を文字列として表示する関数です。
Read
ReadはShowの逆の意味を持つ型クラスです。read関数はRead型クラスのインスタンスに定義されている関数で、文字列を受け取ってその型の値を返します。
1 read "True" || False Trueになる
Enum
Enumは列挙できるようなものを表す型のための型クラスです。Enum型クラスのインスタンスになることで、その型の値をRangeの中身として使うことができるようになります。
Enum型クラスのインスタンスは「後ろ」を表す関数succと「前」を表す関数predを定義しなければいけません。
Num
Numは数の型クラスです。Numのインスタンスは数のようにふるまいます。NumのインスタンスとしてはIntやInteger、Float、Doubleがあります。 掛け算*の型を調べましょう。
1 (*) :: (Num a) => a -> a -> a
*の型をIntやFloatといった特定の型に決め打ちせず、数全体をあらわずNum型クラスのインスタンスならなんでも受け入れるようにすることで
1 5 * 6.5
のような計算もできるようになります。
たとえば
1 (*) :: Int -> Int -> Int
のように型が決め打ちされていれば、5*6.5という計算はできなくなってしまいます。
Floating
Floating型クラスにはFloatとDoubleが含まれます。小数であることを表す型クラスです。
sinやcosといった結果が小数で表現できないと困る関数は、Floating型クラスのインスタンスが返り値の型になるようになっています。
Integral
Integralは整数のみを含む型クラスです。つまりIntとIntegerだけがこの型クラスのインスタンスです。
この型クラスにはfromIntegralという関数があります。
1 fromIntegral :: (Num b, Integral a) => a -> b
これは整数を引数にとって、もっと一般的な整数とも小数ともみなせるような数を返す関数です。この関数を使えば整数と小数を一緒くたに扱いたいときに型が合わずにエラーが出るということがなくなって便利です。
lengthという関数は次のような型を持っています。
1 length :: [a] -> Int
この型のため、リストの長さを取ってきて、それに小数をたそうとするとエラーが起きます。+関数の型を見てみればその理由がわかります。
1 (+) :: (Num a) => a -> a -> a
出てくる型変数が全部同じaであることから、2つの引数と戻り値は同じ型でないといけないことがわかります。いまやろうとしてるのはIntと小数を足すという処理です。 Intと小数は明らかに型が違うので、エラーが出るのです。
そこでfromIntegralという関数を使ってIntを整数とも小数とも解釈できるもっと曖昧な存在にします。こうすることで+関数を使うことができるようになります。
演習
演習1
次に示すものについて型の記述が間違っているかどうかを判定してください。間違っていれば正しく修正してください。
演習2
read "1"を実行してみるとどうなるでしょうか。 またなぜそうなるのかをread関数の型を見て考えてみましょう。
1 read :: (Read a) => [Char] -> a
(余力があれば)read "1"が正しく動くようにプログラムに書き足してみてください。