part4をやるぞ。
Syntax in Functions
関数でパターンマッチングができるとな。数値、文字、リスト、タプルなど何でもパターンマッチングの対象にできる。
lucky :: (Integral a) => a -> String
lucky 7 = "Yukarisan ga sawaideiru!"
lucky x = "Zannnen orz"
いわゆるswitch-case的なのが関数の構文レベルで存在するらしい。マッチングは上から順に実行される。xはefault的な位置づけ。catch-allって書いてある。引数aをIntegralで縛っているから文字や少数なんかは受け取れない。
サンプルもうひとつ。
voiceroids :: (Integral a) => a -> String
voiceroids 1 = "Yukarisan"
voiceroids 2 = "Makimaki"
voiceroids 3 = "ZunZun"
voiceroids 4 = "Futago"
voiceroids x = "Yukkuri"
ここでvoiceroids x
を一番上に持っていくと、あらゆる結果がYukkuri
になる。そりゃそうだ。
実用的な例として紹介されているのが前に出てきたproduct関数をパターンマッチングで実現した版。
factorial :: (Integral a) => a -> a
factorial 0 = 1
factorial n = n * factorial (n-1)
おお。いわゆる再帰的なコードが書けたぞ。
initial :: Char -> String
initial 'd' = "86"
initial 'y' = "Yukarinsan"
initial 'm' = "MakiMaki"
initial 'z' = "ZunZun"
文字によるマッチング。これ自体言うことはないが、catch-allパターンが無いので指定した4文字以外の文字が来るとクラッシュする。というわけでcatch-allは付けたほうがいい、という話。
タプルに対するパターンマッチング
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors a b = (fst a + fst b, snd a + snd b)
2つのタプルを受け取り、タプルを返す関数。ベクトルの足し算。fst, sndとかださいだろ?もっとましな書き方があるぞってことで
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors (x1, y1) (x2, y2) = (x1 + x2 , y1 + y2)
確かにこっちのが分かりやすい。でも定義のほうは->
で、実装は=
となのは何でだろな。
fst
, snd
はpairでしか使えない。なら作ればいいだろ!となり、
first :: (a, b, c) -> a
first (x, _, _) = x
second :: (a, b, c) -> b
second (_, y, _) = y
third :: (a, b, c) -> c
third (_, _, z) = z
_
は、その部分は気にしてない(使うことがない)から省略って感じの意味らしい。
*Main> first ("yukaary", "craft", 3)
"yukaary"
*Main> second ("yukaary", "craft", 3)
"craft"
*Main> third ("yukaary", "craft", 3)
3
実行結果はまあ期待通り。今度はペアのほうが例外吐くんだけど今はまだいいや。
リストに対するパターンマッチング
*Main> let xs = [(1,3), (4,3), (2,4), (5,3), (5,6), (3,1)]
*Main> [a+b | (a,b) <- xs]
[4,7,6,8,11,4]
リストのこの定義は、リスト内の各ペアを足したリストを作るだけ。リストに対しては空リスト[]
及び:
を使ったパターンマッチが可能。
[1,2,3]
は1:2:3:[]
とも表現できる。x:xs
はリストの先頭要素をxに、残りをxsにバインドする。このパターンは長さ1以上のリストにしかマッチしない。’x:y:z:zs’みたいなパターンは長さ3以上のリストにしかマッチしない。
x:xs
のパターンは再帰的な関数でよく使われるが、:
は長さ1以上のリストにしかマッチしないよ。(長さ0の空リストはどうするんだろう…)
というわけでリストに対するパターンマッチングは分かったな。ではhead
関数の定義を見てみる。
myhead :: [a] -> a
myhead [] = error "Fuxxin!! Can't call head on an empty list!"
myhead (x:_) = x
リストが空の場合はエラーを投げる。ので、あんまり使わないほうがいいんだって。タプルの時に出てきた_
も使われている。
リストに対してもう少し有益な情報を出す関数を作る。
tell :: (Show a) => [a] -> String
tell [] = "The list is empty"
tell (x:[]) = "The list has one element: " ++ show x
tell (x:y:[]) = "The list has two elements:" ++ show x ++ " and " ++ show y
tell (x:y:_) = "The list is long. First is:" ++ show x ++ " second is:" ++ show
y
空のリストから順番にマッチングをかけている。パターンマッチングの順序関係はここではあんまり関係無さげ。長さ1のリストは(x:[])
にしかマッチしないだろうし。(x:[])
は[x]
、(x:y:[])
は[x,y]
と書いてもいいが、(x:y:_)
は[]
で囲えない。これは長さ2以上の全てのリストとマッチしてしまう。
リストの長さを出す自作関数はパターンマッチングの再帰で簡単に自作できる。
mylength :: (Num b) => [a] -> b
mylength [] = 0
mylength (_:xs) = 1 + mylength xs
まあこうなるよね。仮にhamという文字列にこのmylength関数を使った場合、以下のような評価が実行される。
- 1 + mylength “am”
- 1 + (1 + mylength “m”)
- 1 + (1 + (1 + mylength “”))
- 1 + (1 + (1 + 0))
続いてsumも実装してみる。mylength
の場合と実装は変わらない。
mysum :: (Num a) => [a] -> a
mysum [] = 0
mysum (x:xs) = x + mysum xs
パターンの中には全要素を参照する@
という記法がある。xs@(x:y:ys)
という表記において、xsはx:y:ys
を参照する。とても単純な一例。
capital :: String -> String
capital "" = "Empty string."
capital all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]
これを実行すると、
*Main> capital "yukarin made TNT to bomb a Maki's house."
"The first letter of yukarin made TNT to bomb a Maki's house. is y"
allで入力した全文を取得できることを確認。
最後に++
はパターンマッチに使用できない。仮に(xs ++ ys)
と指定した場合、何がxsに該当するリスト、何がysに該当するリストか判別できない。(xs ++ [x,y,z])
や(xs ++ [x])
なら意味があるだろうけど、リストの特性上これも無理。
ちょっと確認してみて無理だった。
let xs = [1,2,3]
xs ++ [4,5]
とかなら大丈夫だけど、関数の定義には使えないみたい。
Guaaaaaaaaaaaaaard!!
いわゆるガード条件。BMI(肥満指数だっけ)の判定する超簡単なGuardを使った関数。
chkbmi :: (RealFloat a) => a -> String
chkbmi bmi
| bmi <= 18.5 = "looks like yukarisan, need a bit more weight."
| bmi <= 25.0 = "looks like zunchan, the weight is in normal range."
| bmi <= 30.0 = "looks like makimaki, need an exercise."
| otherwise = "What's a fat pig, you are!!"
otherwise
は予約語みたいでotherwise = True
と定義されているそうな。こいつが無い場合はcatch-allが動かずいずれかの条件に合致しない場合はエラーが投げられる。guard条件には計算式・関数を使用できる。
calcbmi :: (RealFloat a) => a -> a -> String
calcbmi weight height
| weight / height ^ 2 <= 18.5 = "looks like yukarisan,"
| weight / height ^ 2 <= 25.0 = "looks like zunchan."
| weight / height ^ 2 <= 30.0 = "looks lile makimaki."
| True = "You are a pig in capitalism."
実行。
"looks like zunchan."
*Main> calcbmi 45 1.70
"looks like yukarisan,"
*Main> calcbmi 120 1.70
"You are a pig in capitalism."
*Main> calcbmi 100 1.70
"You are a pig in capitalism."
*Main> calcbmi 80 1.70
"looks lile makimaki."
うん。動いている。最初heightの単位が分からんかった。
max, min
なんかはguardを使って書ける。
mymax :: (Ord a) => a -> a -> a
mymax a b
| a > b = a
| otherwise = b
nicemax :: (Ord a) => a -> a -> a
nicemax a b | a > b = a | otherwise = b
nicemax
のほうが短くていいね。compare
の自作版。
mycompare :: (Ord a) => a -> a -> Ordering
a `mycompare` b
| a > b = GT
| a == b = EQ
| otherwise = LT
これが一番読みやすいとは言えそう。
*Main> mycompare 1 2
LT
*Main> 3 `mycompare` 2
GT
Where
calcbmi、weight / height ^ 2
を3回も繰り返すのはナウくないって?whereがあるさ!って話。
nicebmi :: (RealFloat a) => a -> a -> String
nicebmi weight height
| bmi <= 18.5 = "looks like yukarisan."
| bmi <= 25.0 = "looks like zunchan."
| bmi <= 30.0 = "looks like makimaki."
| otherwise = "a ping in capitalism."
where bmi = weight / height ^2
可読性の意味ではniceでも無い気がする。上→下→中と読まなきゃならない。こういうの見るとSQL思い出すな…。
goodbmi :: (RealFloat a) => a -> a -> String
goodbmi weight height
| bmi <= yuka = yuka_msg
| bmi <= zun = "looks like zunchan."
| bmi <= maki = "looks like makimaki."
| otherwise = "a ping in capitalism."
where bmi = weight / height ^2
yuka = 18.5
zun = 25.0
maki = 30.0
yuka_msg = "looks like yukarisan."
whereで定義したものはguardの中でのみ参照可能。あと、where節に列挙する定義は整列しとかないとhaskellが同じブロックにあるかどうかで混乱するとか。
where節はもうちょっとエレガントに書く余地がある。
where bmi = weight / height ^2
(yuka, zun, maki) = (18.5, 25.0, 30.0)
そろそろwhere節の(..)
とタプルの(..)
の区別について混乱してきた。多分、別物。
where節ではパターンマッチも使用できる。
initials :: String -> String -> String
initials "Initial" "d" = "DREAM...."
initials firstname lastname = [f] ++ "." ++ [l] ++ "."
where (f:_) = firstname
(l:_) = lastname
実行。
*Main> initials "Initial" "d"
"DREAM...."
*Main> initials "Yukari" "Yuzuki"
"Y.Y."
1個目はロータリー乗りとして変な使命感に駆られた。whereはパターンマッチのステートメント別に書けそうだね。
関数だって定義できちゃう的な例。
calcbmis :: (RealFloat a) => [(a, a)] -> [a]
calcbmis xs = [bmi w h | (w, h) <- xs]
where bmi weight height = weight / height ^ 2
ペア(要素数2、タイプクラスRealFloatのタプル)の配列を受け取って、RealFloatの配列を返す関数。読み解くのがちょっと難易度上がったな…。
whereによるバインディングはネストができるらしい。どういう形でのネストなのかサンプルコードが欲しい
著者さんはそんなに肥満が心配なのかね。
Let it be
そろそろ頭抱えそう。where
とよく似たlet .. it ..
という構文がある。letはあらゆる場所で変数を束縛するが、きわめて局所的でもある。
cylinder :: (RealFloat a) => a -> a ->a
cylinder r h =
let sidearea = 2 * pi * r * h
toparea = pi * r ^2
in sidearea + 2 * toparea
書き方はlet <bindings> in <expressions>
。letの部分で定義した名前は、その後に続くinの中で利用できる。whereでも似たような表現ができるが、違いは何か? letの場合、最初にbindingsを書いて、その後ろにそれを利用するexpressionを書き出す(whereは逆にパターンマッチングで使用する定義が後ろに来る)。letによるバインディングはwhereとは異なり、if-elseの同じ感覚で使える。
*Main> [if 5 > 3 then "Woo" else "Boo"]
["Woo"]
*Main> 4 * (if 10 > 5 then 10 else 0) + 2
42
*Main> 4 * (let a = 9 in a + 1) + 2
42
うむ。そうか。
*Main> [let square x = x * x in (square 5, square 3, square 2)]
[(25,9,4)]
ローカルスコープの関数を宣言する用途にも使える。
*Main> (let a = 100; b = 200; c = 300 in a*b*c, let foo="Hey"; var = "there" in foo ++ var)
(6000000,"Heythere")
インラインで複数の変数をバインドした場合は;
で区切る。
list comprehension ([x | x <- xs, x > 0]みたいの)
にもlet
は使える。bmiを計算する関数をletを使って書き換えると次のようになる。
letcalcbmi :: (RealFloat a) => [(a, a)] -> [a]
letcalcbmi xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi >= 25.0]
BMIが25以上の要素で構成するリストが得られる。(w, h) <- xs
の部分でbmi
を使用することはできない。この時点では定義されていない。また、list comprehension内ではin
を使う必要はない。letで定義した名前が見える範囲があらかじめ定義されている。
ghci上でもin
は省略できる。省略した場合、letで束縛した変数のスコープはghciセッション全域に渡る。
*Main> let zoot x y z = x * y + z
*Main> zoot 1 2 3
5
*Main> let boot x y z = x * y + z in boot 4 5 6
26
*Main> boot 4 5 6
<interactive>:207:1:
Not in scope: `boot'
Perhaps you meant `zoot' (line 204)
なるほど。boot
はin
の範囲でしか見えていないのか。
letはwhereとは異なり全ガード条件で利用可能なほどスコープが広くないから、うまく使い分けるといいらしい。
Case expressions
Haskellでのcase文はCやJavaより、もう一歩進んでいるらしい。変数の値を元に評価を行うだけでなく、パターンマッチングも利用する。関数定義の際のパラメータに対するパターンマッチングの話を思い出してくれたかね?実は同じことで内部的に変更可能だよ…多分こんな意味。
head1 :: [a] -> a
head1 [] = error "No head for empty lists!"
head1 (x:_) = x
head2 :: [a] -> a
head2 xs = case xs of [] -> error "No head for empty lists!"
(x:_) -> x
言いたいことは分かる。その上でcase使わなくてもよくない?と言いたい。
case expression of pattern -> result
pattern -> result
pattern -> result
...
構文はこんな感じ。パラメータに対するパターンマッチングとは異なる点として、case
はどこでも使える利点がある。サンプル。
desclist :: [a] -> String
desclist xs = "The list is " ++ case xs of [] -> "empty."
[x] -> "a single."
xs -> "a longer list."
出力が同じところをまず書き出して、その後は入力内容に従って文面を変える。where
使って同様の関数を実装するとこうなる。
chklist :: [a] -> String
chklist xs = "The list is " ++ what xs
where what [] = "empty"
what [x] = "a single."
what xs = "a longer list."
where
を使った場合は関数が入れ子になるのか。この程度ならわざわざ関数にするよりcase
のほうが使い勝手が良さそう。
Written with StackEdit.
No comments:
Post a Comment