プログラミングHaskellを読む (3)

第3章 型とクラス

基本

  • Bool, Char, String*1, Int, Integer, Float
  • リスト [a]
    • 要素の型は同じ、型に「長さ」は含まない、長さに制限はない
  • タプル (a, b)
    • 要素の型は自由、型で「要素数*2」が分る
    • 最初の要素は「ユニット」という
    • 素数1のタプルは存在しない*3
  • 関数 (a -> b)
    • 全域関数*4である必要はない
    • カリー化 (a -> b -> c -> ...)
    • カリー化された関数は部分適用により、新たな関数を作成できる。
-- タプルを引数とする関数add
add :: (Int, Int) -> Int
add (a, b) = a + b

-- カリー化された関数add' ("->"が右結合のため括弧は無くても良い)
add' :: Int -> (Int -> Int)
add' a b = a + b

-- カリー化された関数を利用したadd100
add100 :: Int -> Int
add100 = add' 100

{-
空白区切りの関数は左結合
add' 100 200 = (add' 100) 200
-}

多層型

型には変数を利用できる(型変数)。これにより任意の型を表現できる。

*Main> :t length
length :: [a] -> Int
*Main> length [1,2,3]
3
*Main> length "abc"
3
*Main> length [(+), (-), (*)]
3

型変数を一つでも含む型や、その型を持つ式を「多相的」という。

    • 「[a] -> Int」は多相型
    • 「length」は多相関数
  • 標準ライブラリの多相の例
    • fst*5, head, take, zip*6, id*7

多重定義型

型には「クラス制約」を含めることが出来る。

*Main> :t (+)
(+) :: Num a => a -> a -> a

"=>"以前のNum aがクラス制約*8*9
クラス制約を一つでも持つ型を多重定義型という。

    • (-), (*), negate*10, abs*11, signum

数値も多重定義されている。

*Main> :t 3
3 :: Num a => a

クラス

  • 型は関連する値の集合。
    • 下の"Int"や"Int->Int->Int"。
let a = 10 :: Int
add :: Int -> Int -> Int
  • クラスは共通のメソッドを提供する方の集合。
    • HaskellではJava的なクラスメソッドは無い。
    • 関数をその型に適用出来れば、それがクラスメソッド。
    • FractionalクラスはNumクラスのインスタンス
*Main> :t 1.0
1.0 :: Fractional a => a
*Main> :t (+)
(+) :: Num a => a -> a -> a
*Main> (+) 1.0 1.0
2.0
基本的なクラス
  • Eq - 同等
    • (==), (/=)
  • Ord - 順序
  • Show - 表示可能
    • show :: a -> String
  • Read - 読み込み可能
    • read :: String -> a
    • aは明示的に指定するか、文脈で判断される
  • Num - 数値
  • Integral - 整数
    • div, mod
    • divは結果が切り捨てられる!
  • Fractional - 分数
    • (/), recip*12

おまけ

練習問題

1. 何型?
['a', 'b', 'c'] -- [Char]
('a', 'b', 'c') -- (Char, Char, Char)
[(False, 'o'), (True, '1')] -- [(Bool, Char)]
([False, True], ['0', '1']) -- ([Bool], [Char])
[tail, init, reverse] -- [[a] -> [a]]
2. 何型?
second xs = head (tail xs) -- [a] -> a
swap (x, y) = (y, x) -- (a, b) -> (b, a)
pair x y = (x, y) -- a -> b -> (a, b)
double x = x * 2 -- Num a => a -> a
palindrome xs = reverse xs == xs -- [a] -> Bool
twice f x = f (f x) -- (a -> a) -> a -> a

最後のむずいですね。

f x :: a -> b
--"f x"は型"b"を返すので
f (f x) :: b -> c
{-
f :: a -> b
かつ
f :: b -> c
なので
a = b = c
つまり
f :: a -> a
よって
twice f x :: (a -> a) -> a -> a
-}
3. 答えの確認
*Main> :t ['a', 'b', 'c']
['a', 'b', 'c'] :: [Char]
*Main> :t ('a', 'b', 'c')
('a', 'b', 'c') :: (Char, Char, Char)
*Main> :t [(False, 'o'), (True, '1')]
[(False, 'o'), (True, '1')] :: [(Bool, Char)]
*Main> :t ([False, True], ['0', '1'])
([False, True], ['0', '1']) :: ([Bool], [Char])
*Main> :t [tail, init, reverse]
[tail, init, reverse] :: [[a] -> [a]]

*Main> let second xs = head (tail xs)
*Main> let swap (x, y) = (y, x)
*Main> let pair x y = (x, y)
*Main> let double x = x * 2
*Main> let palindrome xs = reverse xs == xs
*Main> let twice f x = f (f x)
*Main> :t second
second :: [a] -> a
*Main> :t swap
swap :: (t1, t) -> (t, t1)
*Main> :t pair
pair :: t -> t1 -> (t, t1)
*Main> :t double
double :: Num a => a -> a
*Main> :t palindrome
palindrome :: Eq a => [a] -> Bool
*Main> :t twice
twice :: (t -> t) -> t -> t

palindromeを間違えた。
比較できないといけないからEq型じゃないとダメだった。

4. 一般的に関数の型をEqクラスのインスタンスにできない理由。逆にどういう場合に実現できるか。

まず、「関数が同等」というのがどういう意味かですが、「同等な引数に対して同等な結果を返す」ものが同等と言えます。

add a b = a + b
add' a b = a + b - b + b

の場合のaddとadd'は、同じ引数を渡した時、結果も同じになるので、同等です。

Eqクラスのインスタンスであれば、当然のことながら比較はできますが、現在時刻を表示するなどの副作用のある関数は返す値が変わります。なので、全ての関数をEqクラスのインスタンスにすることは出来ません。

逆に言えば、副作用が無い関数ではEqクラスのインスタンスにすることは可能だと言えます。

(といった感じでしょうか。。。)

*1:[Char]型じゃないのか?

*2:要素の個数を、リストでは長さ、タプルでは要素数という、らしい

*3:区切りを明確にするための括弧と区別がつかない

*4:全ての入力について出力が定義されている関数

*5:はてなキーワードを参照

*6:zip [1,2] [3,4,5] = [(1,3), (2,4)]

*7:引数にとった値をそのまま返す関数。どういうシチュで使うのだろう…

*8:型変数aの型を指定するイメージ

*9:演算子+をかっこで囲む"(+)"はカリー化された関数になる。演算子については後ほど出てくるみたい

*10:符号の反転

*11:絶対値

*12:reciprocalの略