技術的なやつ

技術的なやつ

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の型

Int

有界整数型。その範囲は処理系依存だが、だいたいは-2^31〜2^31-1なんじゃないかな。
Cとかにおけるint型ですね。

Integer

有界でない整数型。正確にはメモリ上の限界点まで、だろうけど。
引数の階乗を返すfactorialを定義してみる*1

factorial :: Integer -> Integer
factorial n = product [1..n]
-- *Main> factorial 50
-- 30414093201713378043612608166064768844377641568960512000000000000

Float

単精度浮動小数点数*2

Double

倍精度浮動小数点数*3

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のインスタンスが一意に定められない。

感想

型・型クラス辺りは文章で読んでもピンと来ないので、集合論的に考えるとスッキリする。

*1:普通は再帰で書くだろうけど、再帰によるfactorialは第4章で出てくるので、ここはリストのproductを利用する。

*2:たんせいどふどうしょうすうてんすう。良いタイピング練習になった。

*3:ばいせいどふどうしょうすうてんすう。良いタイピング練習になった。

*4:なお、演算子の型をチェックするには丸括弧()で囲む必要がある。たとえば、「:t (==)」。

*5:「aに等値性を与えられること」と「aに大小を与えられること」って同値なんですかね…。同値だとしたら分けてる意味ないし、例外あるんだろうけど。教えてエライ人!