HOME Haskell のお勉強 書き込む

4. 型


Haskell は Lisp や Python と違って強く型付けされた 言語です。Haskell の型はかなり複雑です。

目次

  1. 真偽値、文字、数値
  2. tuple
  3. 関数の型
  4. List
  5. タグ付き型
  6. class と instance

1. 真偽値、文字、数値

まず、真偽値、数値、文字などの要素的な型について 述べます。 以下の表に要素的な型の一覧を示します。
名称 説明
Bool 真偽値です。True と False があります。
Char 文字です。シングルクオートで囲みます。
Int 範囲が決まった整数です。少なくとも (- 229) --- (229 - 1) をカバーします。
Integer 任意精度の整数です。
Float IEEE 単精度の浮動小数点です。
Double IEEE 倍精度の浮動小数点です。

1.1. Bool

Bool には TrueFalse があります。他の多くの言語とは異なり、 偽以外は全て真ということはありません。 厳密にこの2つだけです。

1.2. Char

C 言語と同様に、'a' で1文字を表します。
module Char には以下に示す関数が定義されています。
関数 動作
ord 文字を整数に変換する: ord 'a' --> 97
chr 整数を文字に変換する: chr 97 --> 'a'
isAscii c c < '\x80'
isLatin1 c c <= '\xff'
isControl c c < ' ' || c >= '\DEL' && c <= '\x9f'
isSpace c c `elem` " \t\n\r\f\v\xA0"
isUpper 'A'..'Z'
isLower 'a'..'z'
isAlpha c isUpper c || isLower c
isDigit c c >= '0' && c <= '9'
isAlphaNum [a-zA-Z0-9]
toUpper [a-z] -> [A-Z]
toLower [A-Z] -> [a-z]

1.3. 数値

数値型には 整数型の Int, Integer、浮動小数点の Float, Double、および 有理数、複素数があります。

1,2,3 .. のように整数とも浮動小数点とも解釈できるものは、 処理系が文脈に沿って解釈してくれます。
数値の型を明示するには

value :: type_name
とします。 数値は厳密に型付けされていて、異なる型の数値間の演算は出来ません。

1.3.1. 整数

整数には IntInteger の2種類があります。 Int は精度が決まっている整数です。少なくとも (- 229) --- (229 - 1) の範囲をカバーします。

Integer は任意精度の整数です。 Int で収まらない整数を表します。

整数型の割り算はエラーになります。 fromIntegral で型が特定されていない数に戻してから計算します。

Prelude> (4 :: Int) / (2 :: Int)    -- Error
ERROR - Cannot infer instance
*** Instance   : Fractional Int
*** Expression : 4 / 2

Prelude> (fromIntegral (4 :: Int)) / (fromIntegral (2 :: Int)) -- ok, it returns Num
2.0
Prelude> floor $ (fromIntegral (4 :: Int)) / (fromIntegral (2 :: Int)) -- if you need Int
2

1.3.2 浮動小数点

Float は IEEE 単精度、 Double は IEEE 倍精度の浮動小数点を表します。

1.3.3. 有理数

module Rational に定義されています。いわゆる分数を
分子 % 分母
で表します。
例:
Main> 7 % 3 + 9 % 5
62 % 15

1.3.4 複素数

module Complex に定義されています。複素数を
実部 :+ 虚部
で表します。
例:
Main> (3 :+ 1) + (1 :+ 2)  -- (3+1i) + (1+2i)
4.0 :+ 3.0

1.3.5 数値の型変換

整数型 (Int, Integer)を不特定数値型にするには fromIntegral を使います。
例:
Main> 2.2 + (fromIntegral (2 :: Integer))
4.2
Main> 2.2 + (fromIntegral (2 :: Int))
4.2
その他の型変換については
Haskell 98 report 6.4.6 を見てください。

2. tuple

複数の値を組み合わせるのに使います。tuple を使うと多値を返す関数を作ることが出来ます。 tuple は要素の型の制約はありませんが、要素数は一定である必要があります。

2要素の tuple (pair) には、最初の要素を返す fst と 2番目の要素を返す snd があらかじめ定義されています。

Prelude> fst ("I am first", 2)
"I am first"
Prelude> snd ("I am first", 2)
2
ある要素を取り出す関数を定義するには以下のようにします。
例、4要素の tuple から要素を取り出す。
from_quad i (a0, a1, a2, a3) = case i of
                                   0 -> a0
                                   1 -> a1
                                   2 -> a2
                                   3 -> a3

3. 関数の型

関数の型は入力と出力を矢印でつないで表します。また、関数の型は :: を使って明示します。関数の 型を明示するとコンパイラーが効率的なコードを作ってくれることがあります。
例:
foo :: Int -> Int
foo x = 1 + x
引数が2つ以上ある場合は、矢印をつなげて表します。
例:
hoge :: Int -> Int -> Int
hoge x y = x + y
2つ以上の引数をとる関数に部分的に引数を与えると、与えた引数の数だけ引数が減少した関数が派生します。 例えば、

引数を指定しないで関数を定義することが可能です。これを利用すると高階関数が 簡潔に書けます。
例:

hoge = (+ 2)
baz = (* 3) . (+ 2)   -- baz x = (x+2) * 3

4. List

List は [,] で表します。
tuple と違って、要素数に制限はありません。そのかわり、 要素の型は全て同じである必要があります。
[[a]] などとして、 list の list を作ることが出来ます。 また、[(a,b)] などとして、tuple のリストを作ることも出来ます。

Haskell では文字列は、Char のリスト [Char] として定義されています。

4.1. リストに関わる関数一覧

List は関数型言語の重要なデータ型で、List を扱うさまざまな関数があらかじめ定義されています。
関数名 説明
(:) list の先頭に要素を1つ追加します。3:[2,1,0] → [3,2,1,0]
head list の最初の要素を取り出します。(Lisp の car)
last list の最後の要素を取り出します。
tail 最初の要素を取り除いた list を返します。(cdr)
(!!) list の n 番目の要素を返します。(list !! n)
elem obj が list の要素ならば True を返します。(elem obj list)
take list の最初の n 個からなるリストを返します。(take n list)
drop list の最初の n 個を除いたリストを返します。(drop n list)
dropWhite list の先頭から初めて、predicate が成り立っている間その要素を除外します。 (dropWhile predicate list)
例:
dropWhile (==0) [0,1,0,0,1,1] → [1,0,0,1,1]
(++) リストを連結します。[3,2] ++ [1,0] → [3,2,1,0]
length リストの長さを Integral で返します。length [1,2,3] → 3
reverse リスト を逆順に並べ替えたものを返します。
nub 重複を取り除きます。(module List)
sum 数値からなるリストの要素の和を返します。
product 数値からなるリストの要素の積を返します。
maximum リストの要素の最大値を返します。
minimum リストの要素の最小値を返します。
その他いろいろあります。
Haskell98 Report, Library List を参照してください。

4.2. リストに関わる高階関数

高階関数は、関数を引数にとる関数です。高階関数を使うと抽象化(コードの使いまわし)が 容易になります。リストに関わる高階関数としてはマッピング、フィルタリング、畳み込みがあります。

4.2.1. マッピング

Haskell にはマッピングを行う高階関数 map が定義されています。 この関数は、[1引数の関数] とリストを引数にとり、リストを返します。
-- syntax
map fun ls

-- example
map (+2) [1,2,3,4] → [3,4,5,6]
map (\x -> x + 2) [1,2,3,4] → [3,4,5,6]

4.2.2. フィルタリング

ある集合の要素のうち条件を満たす要素からなる集合を返すことをフィルタリングといいます。 Haskell にはリストのフィルタリングを行う関数 filter が用意されています。
-- syntax
filter predicate ls

-- example
filter (>0) [-2,-1,0,1,2,3,4] → [1,2,3,4]
filter (\x -> x > 0) [-2,-1,0,1,2,3,4] → [1,2,3,4]

4.2.3. 畳み込み

畳み込みは、2引数の関数をリスト全体に作用させることです。 Haskell には畳み込みを行う関数として foldl, foldr の2つが用意されています。 これらの関数は [2引数関数]、初期値、リストの3つの引数を取ります。 foldl と foldr は畳み込みの方法が異なります。foldl では、初期値を先頭にして、 リストの先頭から計算していきます。一方、foldr では初期値を末尾にして、リストの末尾から 計算していきます。
-- syntax
foldl fun val ls
foldr fun val ls

-- example
foldl (-) 10 [1,2,3,4] → 0      -- ((((10 - 1) - 2) - 3 - 4)
foldr (-) 10 [1,2,3,4] → 8      -- (1 - (2 - (3 - (4 - 10))))
ちなみに、リストの最初の要素を初期値とする関数 foldl1, foldr1 があります。

4.3. list の内包表現

mapfilter を両方使うときの簡潔な書き方です。
map (*2) (filter even [1,2,3,4]) → [4, 8] ⇔
[x*2 | x <- [1,2,3,4], even x] → [4, 8]

4.4. 文字列

文字列 (String) は文字の list ([Char]) と同じです。
文字列に関わる主な関数を挙げておきます。
関数名 動作
lines 文字列を1行ずつ分割します。
words 文字列を1単語ずつ分割します。
unlines 文字列のリストを改行文字を挟んでつなげます。
unwords 文字列のリストをスペースを挟んでつなげます。

5. タグ付き型

Haskell ではすでにある型を修飾することによってある特定の性質を持った別の型を作ることが出来ます。 これは他の言語では見られない Haskell 独自の性質です(たぶん)。 そのような型として、IO や Maybe などがあります。

5.1. IO 型

入出力は外界との相互作用であり、関数的に書き表すことは出来ません。 むしろ、入出力は、関数の引数を調べ、また関数の値を表示する手法であり、 関数型プログラムの外側にある事柄です。 しかし、独立したプログラムであるためには、入出力も自前で行う必要が あります。つまり、Haskell は、純粋に関数型の部分 と入出力を行う部分の2つの部分から 成っているということが出来ます。Haskell ではこれら2つの部分がきれいに分離され、関数部分の 参照透明性が保持されています。

関数部分と IO 部分を分離するために、IO の結果として得られる値は 関数部分で使う値と明確に区別する必要があります。そのような区別をするために IO の結果として得られる値には IO というタグをつけます。 通常の変数では、関数の定義によって値が決まるのに対して IO 型の変数は IO が行われるまで値が特定できません。 従って、両者を区別せずに、IO の結果として得られた値を直接関数部分に渡すと、 関数部分の透明参照性が損なわれます。

詳しくは、

  1. A Gentle Introduction to Haskell: IO (日本語訳)
  2. 入出力
を見てください。

5.2. Maybe 型

値を返さないかもしれない関数を記述するとき使います。
Maybe 型は NothingJust a の2種類のデータからなります。 計算が成り立つと Just result_of_calculation を返し、成り立たないと Nothing を返します。 下の例では sqrt の場合を示します。 Maybe 型を返す関数の結果を次の関数に渡したい時は (>>=) を使います。(
Monad で説明します。)
maybe_sqrt :: Double -> Maybe Double
maybe_sqrt x | x < 0 = Nothing
             | otherwise = Just $ sqrt x
Main> maybe_sqrt 2
Just 1.4142135623731
Main> maybe_sqrt (-4)
Nothing

5.3. Either 型

結果によって返す型を変えたいときに使います。 Haskell は強く型付けされているので、関数は基本的に1つの 型しか返せません。
下の例は Int を引数に取り、もし引数が平方数なら Int を返し、 それ以外は Double を返す関数です。 Int を返すときは LeftDouble を返すときは Right を値の前に つけています。
---- sqrt that return Int if possible
int_sqrt :: Int -> (Either Int Double)
int_sqrt x | fsdx * fsdx == x  = Left fsdx  
           | otherwise = Right sdx
 where sdx = sqrt $ fromIntegral x
       fsdx = floor sdx
Main> int_sqrt 35
Right 5.91607978309962
Main> int_sqrt 36
Left 6

6. class と instance

Haskell にも class や instance が定義されています。ただし、これらの定義はいわゆるオブジェクト指向型言語 のものとはかなり異なっています。

6.1. 総称関数

引数の型によって動作を変えるような関数を総称関数といいます。 例えば (==), (>), (<), etc は引数によって比較の方法を変化させています。

6.2. class

Haskell では総称関数の組のことを class と呼びます。平たく言えば、”何々をする方法”ということです。 例えば、class Eq (等しいかどうか調べる方法)には2つの総称関数 (==) と (/=) が定義されています。
class  Eq a  where
    (==), (/=) :: a -> a -> Bool

        -- Minimal complete definition:
        --      (==) or (/=)
    x /= y     =  not (x == y)
    x == y     =  not (x /= y)
また、データ型を(表示させるために)文字列に変換する方法は class Show に書かれています。

6.3. instance

特定のデータ型での総称関数の定義です。総称関数 (==) は それぞれのデータ型で定義されています。

詳しくは 自前の data と class を見てください。

7. より詳しく知るには

型については
  1. A Gentle Introduction to Haskell Classes (日本語訳)
  2. Yet Another Haskell Tutorial
に詳しく書かれています。