技術的なやつ

技術的なやつ

6.3 すごいHaskellたのしく学ぼう! 第3章

Haskellの関数構文について。

パターンマッチ

lucky :: Int -> String
lucky 7 = "You are lucky!"
lucky x = "Ku-z."
{-
*Main> lucky 5
"Ku-z."
*Main> lucky 7
"You are lucky!"
-}

関数定義の上から順にパターンマッチが行われ、マッチした場合それが実行される。
パターンに具体的値でなく、小文字から始まる名前を書くと、任意の値に合致するようになる。引数はその名前に束縛される。
もちろん、if/then/elseを使ってもluckyは定義することができるが、見やすさの問題でこちらの方が優れた定義方法と言える。

階乗を計算するfactorialを、再帰とパターンマッチを使って定義する。

factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial(n-1)
{-
*Main> factorial 5
120
-}

0の階乗は1と定義する。まず、引数が0であるかチェックする。0ならば1を返す。それ以外の整数ならば、n*factorial(n-1)を返す。再帰について詳しくは第4章でやるので、この辺で置いておく。

パターンマッチは、任意の値を取る場合を考えないとエラーでプログラムがクラッシュする場合があるので、最後に任意値についてのパターンマッチを入れておく方が良い設計である

タプル

addVectors :: (Double, Double) -> (Double, Double) -> (Double, Double)
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)
{-
*Main> addVectors (3,4) (5,6)
(8.0,10.0)
-}

リスト

*Main> let xs = [(1,1), (2,3), (3,1), (4,3), (5,1), (6,3)]
*Main> [x | (x,1) <- xs]
[1,3,5]

head関数を定義する

head' :: [a] -> a
head' [] = error "list is empty."
head' (x:_) = x
{-
*Main> head' [1,2,3,4,5,6,7]
1
*Main> head' []
*** Exception: list is empty.
-}

asパターン

値をパターンに分解しつつ、パターンマッチ対象となった値自体を参照したい時に使用する。

headAndString :: String -> String
headAndString [] = error "list is empty."
headAndString xs@(x:_) = "head -> " ++ [x] ++ ", string -> " ++ xs
{-
*Main> headAndString "oh,chin chin"
"head -> o, string -> oh,chin chin"
-}

ガード

パターンマッチは具体的な値でしかマッチングが出来なかったが、ガードでは範囲をしていすることでパターンマッチ(みたいなもの)が可能。
BMI指数を入力すると文字列による評価を返すbmiTellを定義する。

bmiTell :: Double -> String
bmiTell bmi
  | bmi <= 18.5 = "You are too thin!"
  | bmi <= 25.0 = "You look good!"
  | bmi <= 30.0 = "You are fat!"
  | otherwise = "You are a whale!"
{-
*Main> bmiTell 21.0
"You look good!"
*Main> bmiTell 32.6
"You are a whale!"
-}

パイプ文字(|)から続く部分がガードと呼ばれる。パイプ文字以降に条件式を書き、Trueならイコール(=)以降の内容が返される。どのガードも通り抜けた場合、otherwiseで受け止められる。

where

ガードを利用して、体重・身長を入力して計算したBMI指数から、文字列による評価を返すbmiTellを(再)定義する。
なお、BMI指数は体重(kg)を身長(m)の2乗で割った値である。

bmiTell :: Double -> Double -> String
bmiTell weight height
  | weight / height ^ 2 <= 18.5 = "You are too thin!"
  | weight / height ^ 2 <= 25.0 = "You look good!"
  | weight / height ^ 2 <= 30.0 = "You are fat!"
  | otherwise = "You are a whale!"
{-
*Main> bmiTell 52.0 1.698
"You are too thin!"
-}

ガリガリやな、って言われた;-)
しかし、この書き方はイマイチであるのは見れば分かると思う。同じ計算を3回繰り返している。
そこで、whereを使うとすっきりと書ける。

bmiTell :: Double -> Double -> String
bmiTell weight height
  | bmi <= 18.5 = "You are too thin!"
  | bmi <= 25.0 = "You look good!"
  | bmi <= 30.0 = "You are fat!"
  | otherwise = "You are a whale!"
  where bmi = weight / height ^ 2
{-
*Main> bmiTell 52.0 1.698
"You are too thin!"
-}

whereのスコープ

whereのスコープは、属するパターンマッチのみである。ガードは1つのパターンマッチの連続と見なされる。
たとえば、以下の関数を考える。

greet :: String -> String
greet "okabi" = nicegreeting ++ ", okabi!"
greet "gojira" = badgreeting ++ ", gojira!"
greet name = normalgreeting ++ "," ++ name ++ "!"
  where nicegreeting = "Hello"
        badgreeting = "Chinko"
        normalgreeting = "Hi"

この関数は正しく動作しない(というかコンパイルエラーになる)。
whereのスコープは1つのパターンマッチのみなので、この関数ではnicegreeting・badgreeting・normalgreetingはgreet nameから始まるパターンでしか認識できないためである。

whereを使って、もう少しスマートにbmiTellを定義する。

bmiTell :: Double -> Double -> String
bmiTell weight height
  | bmi <= thin = "You are too thin!"
  | bmi <= good = "You look good!"
  | bmi <= fat = "You are fat!"
  | otherwise = "You are a whale!"
  where bmi = weight / height ^ 2
        (thin, good, fat) = (18.5, 25.0, 30.0)

where内の関数

where内では関数も定義できる。体重と身長のリストからBMI指数のリストを返すbmiListを定義する。

bmiList :: [(Double, Double)] -> [Double]
bmiList xs = [bmi w h | (w, h) <- xs]
  where bmi weight height = weight / height ^ 2
{-
*Main> bmiList [(52.0, 1.698), (85.0, 1.752), (32.0, 1.519)]
[18.03549107173825,27.69177039678072,13.868657743630061]
-}

let式

whereが後から変数や関数を定義していたのに対し、前から定義するlet式がある。
「式」と名のつく通り、let自体が値を返すという点でwhereと明確に異なる。「式」だから、whereのようにガードをまたぐことはできない。
円柱の表面積を高さと半径から求めるcylinderを定義する。

cylinder :: Double -> Double -> Double
cylinder h r = 
  let sideArea = 2 * pi * r * h
      topArea = pi * r ^ 2
  in  sideArea + 2 * topArea
{-
*Main> cylinder 3 4
175.92918860102841
-}

let式は、let A in Bという形を取る。ここで定義した値はlet式全体でのみ参照できる。
GHCiにおいてinを省略した場合は、全体から参照可能になる。GHCiでlet式を定義した時に、定義した値が返るのは、let式が「式」だからである。

また、let式は「式」なので、リスト内包表記で利用することもできる。
体重と身長のペアのリストから肥満な人のBMIだけをリスト化するbmiList'を定義する。

bmiList' :: [(Double, Double)] -> [Double]
bmiList' xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi > 25.0]
{-
*Main> bmiList' [(52.0, 1.7), (55.0, 1.7), (60.0, 1.7), (70.0, 1.7), (80.0, 1.7), (90.0, 1.7), (100.0, 1.7)]
[27.68166089965398,31.14186851211073,34.602076124567475]
-}

case式

Cとかのswitchでお馴染みのcase。Haskellではコイツも「式」なので、いろいろな使い方ができる。
リストの先頭要素を返すheadを、case式を使って定義する。

myHead :: [a] -> a
myHead xs = case xs of [] -> error "list is empty."
                       (x:_) -> x
{-
*Main> myHead "Chinko"
'C'
-}

case Expression of Pattern1 -> Result1...という書き方になる。of以降のパターンはインデントを揃えないとエラーが起きたので注意。
case式全体が値を返すのでこのような書き方もできる。

describeList :: [a] -> String
describeList xs = "This list is "
                  ++ case xs of [] -> "empty."
                                [x] -> "a singleton list."
                                ls -> "a longer list."
{-
*Main> describeList ""
"This list is empty."
*Main> describeList "c"
"This list is a singleton list."
*Main> describeList "chinchin"
"This list is a longer list."
-}

感想

  • whereは後から定義を補完するもの。
  • let A in B は全体が1つの式となり、その中で利用可能なモノをA内で定義、Bがlet式全体の値となる。GHCiにおいてinを省略した場合は、A自体がlet式全体の値となり、スコープが全体になる。
  • case A of B -> C は全体が1つの式となり、以前定義されているAがBのパターンにマッチする場合Cをcase式全体の値として返す。

式とそうでないものの違いをハッキリ認識することが大切であるようだ。式は必ず値を返す。
だから、ifもif「式」って呼んでたんですね。
パターンマッチの強みは、大きく捉えているもの(たとえばタプルとかリストとか)から部分を取り出して、そこを評価できること。これが出来るのはwhereおよびlet式。caseでは部分評価はできない。
あまり深く考えると頭がおかしくなりそうだから、適当に組んでいくうちにもっと理解が深まるだろう…。というわけで次に進もう。