てきとうなメモ

本の感想とか技術メモとか

Haskellの勉強 (3) - 関数を定義する

パターンマッチ

リストの長さを求める関数lengthを考える

length [] = 0
length (x:xs) = 1 + length xs

'x:xs'はxがリストの最初の部分を表し,xsがリストの最後の部分を表す.これは,「空リストならば0を返し,x:xsの形であれば1+(xsのリストの長さ)の値を返す関数」を表している.引数が'[]'の形であるか,'x:xs'の形であるかパターンマッチを行いで条件分岐を行っている.

他にも,階乗関数factは次のように定義できる.

fact 0 = 1
fact n = n * fact n - 1

関数定義の順番は重要.上から順にパターンマッチが行われるので,

fact n = n * fact n - 1
fact 0 = 1

と記述すると,無限ループに陥る.

case式

factは次のようにも書ける.

fact n = case n of
           0 -> 1
           n -> n * fact (n - 1)

これもパターンマッチで条件分岐を行っている.

インデント

Haskellではインデントが重要な意味を持つ.インデントがずれていると構文解析に失敗する.

先程のfactならば

fact n = case n of
           0 -> 1
            n -> n * fact (n - 1)

のようにインデントがずれていると,

Prelude> :load fact2.hs
Compiling Main             ( fact2.hs, interpreted )

fact2.hs:3:14: parse error on input `->'
Failed, modules loaded: none.

parse errorが起こる.

if式

if式も値を持つため,else部は省略で来ない.

abs x = if x > 0 then
            x
        else
            if x == 0 then
                0
            else
                -x

ガード節 (guard clause)

同様の関数を以下のように簡単に定義できる.

abs x | x > 0 = x
      | x == 0 = 0
      | x < 0 = -x

'|'で分割された部分をガード(guard)と呼ぶ.

lambda式

Schemeのlambda式

(lambda (x) (+ x 1))

Haskellでは以下のように表現できる.

\x -> x + 1

これはインクリメントを行う.

引数に適用すると

Prelude> (\x -> x + 1) 1
2
Prelude> (\x y -> x + y) 2 3
5

関数の型

関数の型は'引数の型->返り値の型'で表現される.

Prelude> :t (\x -> x + 1)
(\x -> x + 1) :: (Num a) => a -> a
Prelude> :t cos
cos :: (Floating a) => a -> a

(Floating a)は浮動小数点数の型aに対してのような意味.

カリー化(curry)

引数が複数であると

Prelude> :t (\x y -> x + y)
(\x y -> x + y) :: (Num a) => a -> a -> a

のように'->'が複数出てくる.'->'は右結合の演算子として見る

(Num a) => a -> (a -> a)

となるので,これはaという引数を取り,a->a(aという引数を取りaを返す関数)を返す.

さらにこの関数に引数を適用すると

Prelude> :t (\x y -> x + y) 1
(\x y -> x + y) 1 :: (Num a) => a -> a

となり,1を足す関数のlamda式を表現できる.このように引数を途中まで適用した状態にすることをカリー化と呼ぶらしい.

型推論 (type inference)

Haskellではコンパイラ自動的に関数の型を推論してくれる.例えば,関数lengthは

Prelude> :t length
length :: [a] -> Int

aのリストを引数に取りIntを返す関数であると推論される.ここで,aは任意の型であり,CharであってもIntであっても何かのリスト構わない.このように,できるだけ抽象的というか一般性の高い型を推論する.

型宣言(type signature)

コンパイラに推論してもらうのではなく,自分で型を決める方法もある.これを型宣言と呼ぶ.型宣言は'::'を用いて行う.

length :: [a] -> Int

文字列だけに対応したlength,strlenを定義したい場合は

strlen :: [Char] -> Int

とする.

Prelude> :l strlen.hs
Compiling Main             ( strlen.hs, interpreted )
Ok, modules loaded: Main.
*Main> strlen "Haskell"
7
*Main> strlen [0,1,2]

<interactive>:1:8:
    No instance for (Num Char)
      arising from the literal `0' at <interactive>:1:8
    Probable fix: add an instance declaration for (Num Char)
    In the list element: 0
    In the first argument of `strlen', namely `[0, 1, 2]'
    In the definition of `it': it = strlen [0, 1, 2]

高階関数 (higher-order function)

Haskellでは関数を関数の引数として扱える.たとえばリストの個々の要素に関数を適用したリストを返す関数mapは

map f [] = []
map f (x:xs) = f x : map f xs

のように表現できる.このように,引数として関数をとる関数を高階関数と呼ぶ.

この関数はの型は以下の形式になる.

Prelude> :t map
map :: (a -> b) -> [a] -> [b]

つまり,「aを引数に取りbを返す関数(a->b)」と「aのリスト([a]」を引数に取り,「bのリスト[b]」を返す関数である.

局所化

変数のバインドを局所化するためにはletかwhereを用いる.例えば二乗の和を求めるsumOfSquaresは以下のように定義できる.

sumOfSquares x y = (square x) + (square y) 
    where square x = x * x
sumOfSquares2 x y = let square x = x * x 
                    in (square x) +  (square y)

演算子

演算子も関数である.中置演算子を前置記法で用いるときは'()'で囲む

Prelude> (+) 2 3
5
Prelude> (++) "Hello, " "Haskell"
"Hello, Haskell"

型を調べると以下のようになる

Prelude> :t (+)
(+) :: (Num a) => a -> a -> a
Prelude> :t (++)
(++) :: [a] -> [a] -> [a]

中置演算子に引数を部分適用したものをセクション(section)と呼ぶ.Haskellではこれを(1+)や(+1)の形式で記述できる.

Prelude> (1+) 2
3
Prelude> (+1) 2
3

中置演算子のどちらに引数を置くかは重要であり,以下のように変化する.

Prelude> (2/) 4
0.5
Prelude> (/2) 4
2.0

逆に通常の関数を中置演算子のように扱いたいときは``を用いる.

add = (+)

Prelude> :l operator.hs
Compiling Main             ( operator.hs, interpreted )
Ok, modules loaded: Main.
*Main> add 2 3
5
*Main> 2 `add` 3
5