プログラミングHaskell第2版 4章メモ書き

4章まできた! パターンマッチとかラムダとか勉強するときに、昔ちょっとだけ勉強したOCamlの知識が役に立ってよかった。 練習問題も少しずつ難しくなってきたが今の所なんとかやっている。

条件式

Haskellの条件式には、常にelse部が必要。(これによって「ぶら下がりelse問題」が回避できる)

signum :: Int -> Int
signum n = if n < 0 then -1 else
              if n == 0 then 0 else 1

ガード付きの等式

条件式の代わりに、ガード付きの等式を使って関数を定義することもできる。

条件式と比べて、条件が多くなった時に読みやすい点が優れている。

もし最初のガードがTrueなら最初の候補、そうではなく2つ目のガードがTrueなら2つ目の候補、という具合に候補が選ばれる。

otherwiseは、プレリュードで単にotherwise = Trueと定義されているため、「他の全ての場合」を処理するのに便利。どのガードもTrueにならない場合はエラーが発生するが、これはotherwiseによって回避できる。

signum n | n < 0     = -1
         | n == 0    = 0
         | otherwise = 1

パターンマッチ

パターンと呼ばれる式に基づいて、列挙された同じ型の候補の中から結果が選ばれる。

-- 否定演算子notのパターンマッチを使った定義
not :: Bool -> Bool
not False = True
not True  = False

-- 論理積を計算する&&演算子のパターンマッチを使った定義
(&&) :: Bool -> Bool -> Bool
True  && b = b
False && _ = False

-- `_` はワイルドカード。上の例は、1つ目の引数がTrueであれば結果は2つ目の引数、1つ目の引数がFalseであれば
-- 結果はFalseであるという意味。

タプル・パターン

パターンを要素として持つタプル。

「要素数が同じで、それぞれの要素が対応するパターンに全て合致するタプル」に合致する。

-- 組の1つ目の要素を返す関数
fst :: (a, b) -> a
fst (x, _) = x

-- 組の2つ目の要素を返す関数
snd :: (a, b) -> b
snd (_, y) = y

リスト・パターン

パターンを要素として持つリスト。

「長さが同じで、それぞれの要素が対応するパターンに全て合致するリスト」に合致する。

-- リストの長さが3で、かつ先頭の要素が文字'a'であるかを検査する関数
test :: [Char] -> Bool
test ['a', _, _] = True
test _           = False

リストの合成とcons演算子

Haskellのリストは合成されたデータであり、空リスト[]に対して演算子:を使って要素を1つずつ増やしていくことで生成される。

:は既存のリストの先頭に新しい要素を追加して新しいリストを生成する演算子で、cons演算子と呼ばれる。

-- リスト[1, 2, 3]は次のように分解できる
  [1, 2, 3]
= 1 : [2, 3]
= 1 : (2 : [3])
= 1 : (2 : (3 : [])) -- = 1 : 2 : 3 : [], cons演算子は右結合

cons演算子によるパターン

先頭の要素と残りのリストがそれぞれ対応するパターンに合致するような、空でないリストに合致する。

-- 空でないリストの先頭の要素を取り出す
head :: [a] -> a
head (x:_) = x

-- 空でないリストから先頭の要素を取り除く
tail :: [a] -> [a]
tail (_:xs) = xs

-- 関数適用は演算子よりも結合順位が高いので、cons演算子を使ったパターンは括弧で囲まなくてはならない

ラムダ式

無名関数のこと。なので関数名は持たない。

引数のパターンと、引数から結果を計算する方法を示した本体とからなる。

-- 数値の引数xを取り、x + xを計算して返す無名関数の作成
-- `\`はギリシア文字の`λ`を表している
\x -> x + x

-- 関数名はないが、他の関数と同じように利用できる
  (\x -> x + x) 2
= 4

ラムダ式を使うと、カリー化された関数の型注釈と関数定義を同じ形にすることができる

add :: Int -> (Int -> Int)
add = \x   -> (\y -> x + y)

「関数を返す関数」を定義するときに、カリー化された結果ではなく本来の関数が結果として返される場面でも便利

-- プレリュード関数constは関数を返し、その関数は引数が何であれあらかじめ定められた値を返す
const :: a -> b -> a
const x _ = x

-- 上は、型宣言に括弧をつけ、定義にラムダ式を用いれば、constが返り値として関数を返すことが明確になる
const :: a -> (b -> a)
const x = \_ -> x

一度しか参照されない関数に名前を付けずに済む

-- 最初のn個の正の奇数を返す関数
odds :: Int -> [Int]
odds n = map f [0..n-1]
         where f x = x*2 + 1

-- example
odds 4 #=> [1, 3, 5, 7]

-- 次のように簡略化できる
odds :: Int -> [Int]
odds n = map (\x -> x*2 + 1) [0..n-1]

セクション

+のように、引数の間に置かれる関数を演算子と呼ぶ。

引数を2つ取る関数は、7 div 2 のように関数名をバッククォートで囲むことで演算子になる。

逆に、任意の演算子を()で囲むことによって引数の前に置いて使うカリー化された関数とすることも可能。

(+) 1 2 #=> 3

また、必要であれば(1+) 2、(+2) 1のように引数の1つを括弧の中に入れることもできる。

'#'を演算子とすると、引数xとyに対し、式(x), (x #), (# y)は一般にセクションと呼ばれる。

セクションの関数としての意味は、ラムダ式を使って次のように形式化できる。(このへん結構好き)

(#) = \x -> (\y -> x # y)
(x #) = \y -> x # y
(# y) = \x -> x # y

セクションの用途

単純で有益な関数を簡潔に定義する

(+) 加算関数
(1+) 引数に1を加える関数
(*2) 引数を倍にする関数

etc...

演算子の型を宣言する(Haskellでは演算子そのものは正しい式ではないため)

(+) :: Int -> Int -> Int

他の関数に二項演算子を引数として渡す(これ理解できてない)

sum :: [Int] -> Int
sum = foldl (+) 0

練習問題の回答はこちら

https://gist.github.com/h-shima/1e6c22c4fa655940009b60b436f0f886