Saturday, July 26, 2014

Haskell part4

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)

なるほど。bootinの範囲でしか見えていないのか。

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