6.2 すごいHaskellたのしく学ぼう! 第2章
第2章はHaskellの型システムについて。
明示的な型宣言
GHCiでは、:tを使うことで型を調べることができる。
Prelude> :t 'a' 'a' :: Char Prelude> :t True True :: Bool Prelude> :t "Hello" "Hello" :: [Char] Prelude> :t 4 == 5 4 == 5 :: Bool Prelude> :t ('a', True) ('a', True) :: (Char, Bool)
Haskellにおける"::"は、「の型を持つ」と読むことができる。
この中で注目すべきは、文字列とタプル。"Hello"はCharのリストとして表現されている。('a', True)は(Char, Bool)という型である。タプルは、要素の数と要素の型の組み合わせで1つの新しい型となる。
Haskellでは、型名はすべて大文字から始まる。
関数に明示的な型を与える
removeNonUppercase :: [Char] -> [Char] removeNonUppercase st = [c | c <- st, c `elem` ['A'..'Z']]
1行目の表現で、「removeNonUppercaseはCharのリストからCharのリストへの写像である」と読むことができる。
なお、removeNonUppercaseは与えられた文字列から大文字以外を除去した文字列を返す関数である。
引数が2つ以上ある場合は以下のように書けば良い。addThreeは、与えられた3整数をすべて足しあわせて返す関数。
addThree :: Int -> Int -> Int -> Int addThree x y z = x + y + z
一般的なHaskellの型
Integer
有界でない整数型。正確にはメモリ上の限界点まで、だろうけど。
引数の階乗を返すfactorialを定義してみる*1。
factorial :: Integer -> Integer factorial n = product [1..n] -- *Main> factorial 50 -- 30414093201713378043612608166064768844377641568960512000000000000
Bool
真理値型。TrueかFalseしか取れない。
Char
Unicode文字型。文字をシングルクォートで囲む。Charのリストは文字列型。
タプル
タプルの1つ1つが型。一度に最大62個までしか定義できないらしいが、そんなに必要になることはまずないとのこと。空タプルを1つのタプルだとしても合計63個だが、あと1つはなんだろう。
型変数
リストの先頭要素を取り出すheadの型を見てみる。
*Main> :t head head :: [a] -> a
ここで使われている「a」は小文字から始まっているので型ではない。これは型変数と呼ばれる。C++でいうテンプレートみたいなもの。
型変数名は小文字から始まっていれば何でもいいが、1文字のみで表すことが多い。また、1つの型宣言中に同じ文字が使われている場合、それらの型は同じであることを意味する。
型クラス
…だけど、型変数では少し厄介なことが起こる。
たとえば、第1章でsucc(引数の次の要素を返す)という関数が出てきたが、これに(3,2)というタプルを与えるとどうなるだろうか。「次の要素」が定義されていないので、どうしようもない。
そこで、型変数に制約を与えたい。具体的には、「順番」が存在している型(つまり可算集合)のみを取れるようにしたい。
succの型を見てみよう*4。
succ :: Enum a => a -> a
ここで、"::"以降の記述は「aはEnumクラスのインスタンスであり、aからaへの写像である」と読める。Enumクラスは性質として順番が定義できる。
型を統合させるものが型クラスである。すなわち、以下のような構造をしている。
Haskellの型クラス
Eq
等値性を持つクラス。==および/=による比較が可能。
Ord
大小比較ができるクラス。>および<による比較が可能。なお、その性質上Eqクラスに部分集合として含まれる*5。
Show
文字列に変換可能なクラス。たとえば、
*Main> show 1234 "1234"
Read
文字列から変換可能なクラス。たとえば、
*Main> read "True" || False True
Enum
順番があるクラス。
Bounded
有界なクラス。
Num
数を表すクラス。加減乗除が定義可能。
Floating
浮動小数点数を表すクラス。
Integral
整数を表すクラス。
型推論
ここで、fromIntegralという関数の型を調べる。
*Main> :t fromIntegral fromIntegral :: (Integral a, Num b) => a -> b
すなわち、与えた整数を何らかの数型に変換する関数である。
たとえば、以下のような状況で使用する。
*Main> length [1,2,3,4] + 1.0 # Error。length :: [a] -> Int なので、IntとFloatの足し算になるため。 *Main> fromIntegral( length [1,2,3,4] ) + 1.0 5.0 # FloatはNumのインスタンスなのでOK。
…と、このようにfromIntegralが返す型は明示的に宣言されていないが、Haskellは状況に応じて必要な型を推論し、与えられたクラスから適当なインスタンスを決定する。これを型推論という。
Haskellは非常に強い静的型付けを持ちつつ、強力な型推論も備えている。
型が一意に定まらない場合、Haskellはエラーを返す。たとえば、以下のような例を考えると良い。
*Main> read "5" - 2 3 # 2がIntなので、read "5" はIntとして一意に定まる。 *Main> read "5" # エラー。Numのインスタンスが一意に定められない。
感想
型・型クラス辺りは文章で読んでもピンと来ないので、集合論的に考えるとスッキリする。