twitter: @koko_u
名前: 小崎つねあき
仕事: SI屋さん
Haskell歴: 1ヶ月
関数定義(p.5)は
add :: Int -> Int -> Int add x y = x + y
のように、関数名と引数リストと定義する式を = で繋ぎます
引数がなくてもよい。その場合は定数(あるいは変数)ということ
"=" は他の言語での「代入」ではなく、同値関係を表している
第1章、第2章で登場した要素で式とは
数値、文字、文字列
10 'a' "hello"
リスト、タプル
[1, 2, 3] ('a', "young")
if .. then .. else
if pi == 3 then "ゆとり" else "おっさん"
関数適用
max 3 7 (+) 10 2
let式
let incr n = n + 1 in incr 100
case式
case list of [] -> "empty" [x] -> "single list" _ -> "uncountable list"
関数を定義する場合は型宣言
add :: Int -> Int -> Int
をするのが良い習慣(p.24)と書かれていますが、型宣言をすることでどのような利点があるのでしょうか?
関数と変数は基本的には同じで、許されるのは「英数字」「_」「'」
先頭文字は「英小文字」(大文字はだめ)「_」
関数とは別に演算子(中置演算子)が定義できて、これは逆に「記号」のみから構成される。
x ★ y = x * y
とかユニコードの記号でもイケる。でもどの位までOKなのかよくわからん。
関数を定義する時に、引数の「構造」に応じて定義式を場合分けする。
今の時点では引数のデータ構造としては
程度しかないので、強力さを実感しにくい。
単純な値のマッチ
lucky 7 = "lucky seven" lucky x = "normal number"
タプルのマッチ
addVector (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)
再帰に便利
mySum [] = 0 mySum (n:ns) = n + mySum ns
関数を定義する時に、引数の性質に応じて定義式を場合分けする。普通に if 文を書くのと同じような感覚で
bmiTell bmi | bmi <= 18.5 = "You're underweight" | bmi <= 25.0 = "You're normal.\ \ I bet you're ugly!" | bmi <= 30.0 = "You're fat!" | otherwise = "You're a whale!"
if 式で書くと残念な感じになるのでガードを使いましょう。
bmiTell bmi = if bmi <= 18.5 then "You're underweight" else if bmi <= 25.0 then "You're normal.\ \ I bet you're ugly" else if bmi <= 30.0 then "You're fat!" else "You're a ..."
条件が並列に書けるのでガードの方が読みやすい
ガードの定義で出てきた otherwise も関数で実体は True と同じ。そりゃそうだ。
文字列中で \ と \ の間の空白は無視される。長い文字列を折り返すのに使うみたい。
pangram = "\ \The quick brown fox\n\ \jumps over the\n\ \lazy dog.\n\ \"
関数を定義する時、関数定義のスコープ内でしか使用しない変数(=関数)を定義するのに使う
bmiTell weight height | bmi <= 18.5 = "You're underweight" | bmi <= 25.0 = "You're normal.\ \ I bet you're ugly!" | bmi <= 30.0 = "You're fat!" | otherwise = "You're a whale!" where bmi = weight / height ^ 2
where節で関数を定義してもいい。変数と関数は同じ
bmiTell weight height | skinny bmi = "You're underweight" | normal bmi = "You're normal.\ \ I bet you're ugly!" | fat bmi = "You're fat!" | otherwise = "You're a whale!" where bmi = weight / height ^ 2 skinny x = x <= 18.5 normal x = x > 18.5 && x <= 25.0 fat x = x > 25.0 && x <= 30.0
関数の定義は以上。
関数を定義する時に使う定義式(=右辺)の構成要素として新たに、「let式」と「case式」を使うことができる。
これらは「式」なので別段関数定義に特化した話ではない。 敢て言うなら、case 式は関数定義をするまでもないその場限りのパターンマッチと考えることもできる。
しかし p.49 は明らかに関数 what を定義した方が見易い。
関数定義とはあまり関係ないが、ちょいちょいリスト内包表記の話が差し込まれていて読み難い。
リスト内包表記のジェネレータでパターンマッチが使える。
マッチしない要素はスキップされる
let xs = [(1,3), (4, 3), (2, 4)] [a + b | (a, b) <- xs ]
let でリスト内包表記の出力部分やフィルターに使用できる変数や関数をバインドすることができる
fatMen xs = [ p | p@(w, h) <- xs, let bmi = w/h^2; ⇨ fat x = x > 25.0, fat bmi ]
リスト内包表記でマッチしないパターンは除外される
ghci> let ls = [(1,2),(3,4),(5,7),(3,8)] ghci> [ p | p@(3, _) <- ls ] [(3, 4), (3, 8)]
...
なるほど、でこれを利用して関数を定義してしまう
choice :: Int->[(Int,Int)]->[(Int,Int)] choice n ls = [ p | p@(n, _) <- ls ]
ghci> choice 3 [(1,2),(3,4),(5,7),(3,8)] [(1,2),(3,4),(5,7),(3,8)]
...
choice :: Int->[(Int,Int)]->[(Int,Int)] choice n ls = [ p | p@(n, _) <- ls ]
リスト内包表記中の p@(n, _) <- ls は全てがマッチ
関数 choice の"引数" n とは別物!"代入"されたりしない
再帰がどんなものかわかればよい。
初めて「再帰関数」を見た人は各自で第4章の例を写経して下さい。
簡単ではない。特に遅延評価やら無限リストを考えると頭が痛くなる。
与えられた文字列を大文字に変換する関数を書きなさい
ghci> upperCase "Hello World" "HELLO WORLD"
与えられた文字列から空白を除去する関数を書きなさい
ghci> removeSpace "Hello World\nI'm John." "HelloWorldI'mJohn."
リスト lst1 が lst2 を含んでいるかどうかを判定する関数を書け
ghci> [1..4] `cover` [2..4] True ghci> [1..4] `cover` [2..6] False
重複も考慮
ghci> [1, 1, 2] `cover` [1, 1] True ghci> [1, 2] `cover` [1, 1] False
配列から重複する要素を削除する関数を書け
ghci> uniq [1, 2, 1, 7, 6, 2, 4, 8] [1, 2, 7, 6, 4, 8]
順序は気にしなくてよいとしよう。
与えられた正数から二進表記の文字列を得る関数を書きなさい
ghci> binExp 10 "1010"
マイナスは、まあいいか。
ハノイの塔
三つのポール 1 2 3 の内、ポール 1 に n 個の円盤が乗っているとし、すべての円盤をポール 2 に移動する手順を返す関数を書け。
手順はポール 1 からポール 2 に円盤を移動する場合にペア (1, 2) と表記し、そのようなペアのリストを手順と見なす
ghci> hanoi 3 [(1, 2), (1, 3), (2, 3), (1, 2), (3, 1), (3, 2), (1, 2)]
第 n 番目のフィボナッチ数を得る関数を書け、ただし 1番目と 2番目のフィボナッチ数は 1 とする
ダメな例
fib :: Integer -> Integer fib 1 = 1 fib 2 = 1 fib n = fib (n-1) + fib (n-2)
fib 50 くらいで還らぬ人となります。
縦に n マス、横に k マスの碁盤目をAからBまで最短経路で移動する場合の数を求めよ
ダメな例...もういいよね。
pathCount _ 0 = 1 pathCount 0 _ = 1 pathCount n k = pathCount (n-1) k + pathCount n (k-1)