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