タコさんブログ

プログラミングメモと小言

すごいHaskell 第3章 関数の構文 メモ

この章では関数のパターンマッチ、ガード、where、let式、case式 について書いてある。

パターンマッチ

パターンマッチはパターンに従って、データを分解するために用いる。 関数の本体を下の例のように分解することもできる。 パターンは上から順に評価される。パターンに小文字から始まる名前を書くと、任意の値にマッチするようになる。

luckySeven :: Int -> String
luckySeven 7 = "LUCKY NUMBER SEVEN!"
luckySeven x = "Sorry, you're out of luck, pal!"

これは if/else で下のように書くのと同じ。

luckySeven :: Int -> String
luckySeven x = if x == 7  
          then "LUCKY NUMBER SEVEN!"
          else "Sorry, you're out of luck, pal!"

パターンマッチを使った再帰関数の例 (factorial)

factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)

次の例はNon-exhaustive patterns in function(パターンが網羅的でない)とエラーがでる。

charName :: Char -> String
charName 'a' = "Albert"
charName 'b' = "Broseph"
charName 'c' = "Cecil"

a, b, c以外のパターンにマッチしない場合の処理が抜けているので、エラーになる。関数型言語の関数は何らかの値を返さないといけないので、エラーとなる。if/else で考えると else の処理がないのと同じ意味。上の例のエラーをなくすには

charName _ = "Other"

のように、'a', 'b', 'c' 以外のパターンの処理を書く必要がある。

Haskellは遅延評価を行うので、ファイルを読み込んだ時点ではエラーは表示されない。つまり、

charName 'h'

と評価するまで、エラーにはならない。.hsファイルの先頭に下のオプションを追加することで、エラーチェックをGHCが行ってくれる。

{-# OPTIONS -Wall -Werror #-}

タプルのパターンマッチ

2次元ベクトルの和をパターンマッチを使用しないで、書くと次のように書ける。

addVectors :: (Double, Double) -> (Double, Double) -> (Double, Double)
addVectors a b = (fst a + fst b, snd a + snd b)

引数の a b の部分にパターンマッチを使用して書くと、

addVectors :: (Double, Double) -> (Double, Double) -> (Double, Double)
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)

より自然になる。

リストのパターンマッチとリスト内包表記

リスト内包表記にもパターンマッチが使用できる。

let xs = [(1,3), (4,3), (2, 4), (5,3), (5,6), (3,1)]
[a+b | (a, b) <- xs] -- 出力:[4,7,6,8,11,4]

リスト [1,2,3] は 1:2:3:[] の構文糖衣なので、リストも次のようにして、パターンマッチで使える。

let x:xs = [1..10]
x  -- 1
xs -- [2,3,4,5,6,7,8,9,10]
  • リストに対してパターンマッチを使った関数の例
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. The first two elements are: " ++ show x ++ " and " ++ show y

asパターン

asパターンを使えばパターンマッチの対象となった値自体も参照できる。 asパターンの構文はパターンの前に @ (アットマーク)をつけて、参照する変数名を書く。

variable_name@(some_pattern)
let all@(x:xs) = [1,2,3,4]
all -- 出力:[1,2,3,4]
x -- 出力:1
xs -- 出力:[2,3,4]

場合分けして、きっちりガード!

関数の引数によって場合分けをするときは、ガードを使う。ガードはif文と似ているが、可読性が高い。 ガードは以下のような構文になる。

function_name param
  | expression1 = return value if expression1 is TRUE
  | expression2 = return value if expression2 is TRUE
  | otherwise = return value if the above expressions are FALSE
  • 肥満度指数(BMI)から叱り方を返す例
bmiTell :: Double -> String
bmiTell bmi
  | bmi <= 18.5 = "Your're underweight."
  | bmi <= 25.0 = "You're normal."
  | bmi <= 30.0 = "You're fat."
  | otherwise = "You're whale."
  • max関数の例
max' :: (Ord a) => a -> a -> a
max' a b
  | a <= b = b
  | otherwise = a
  • compare関数の例
compare' :: (Ord a) => a -> a -> Ordering
compare' a b
  | a == b = EQ
  | a <= b = LT
  | otherwise = GT

Where?!

関数の最後ににwhere キーワードを使って値を変数に束縛(バインド) することができる。

let x = 1 + y + z where y = 1; z = 1
x -- 出力:3

※ where節で複数の変数を使うとき、インデントを揃えないとエラーになる

Whereのスコープ

  • 関数の外からは見えない
  • 関数の違うパターンの本体からは見えない
    次の例はniceGreetingがスコープの範囲外というようなエラーになる
 greeting :: String -> String
 greeting "Juan" = niceGreeting ++ " Juan!!"
 greeting "Fernando" = niceGreeting ++ " Fernando!!"
 greeting name = badGreeting ++ " " ++ name
     where niceGreeting = "Hello! So very nice to see you,"
           badGreeting = "Oh!  Pfft. It's you."

whereとパターンマッチ

whereの中でもパターンマッチが使用できる。次の関数initalsはfirstnameとlastname を引数にとり、whereの中でパターンマッチを行い、イニシャルを返す。

initials :: String -> String -> String
initials firstname lastname = [f] ++ "." ++ [l]
    where (f:_) = firstname
          (l:_) = lastname

whereブロックの中の関数

whereブロックの中では関数も定義できる。 次の関数calcBmisは体重と身長のペアのリストを受け取ってBMIのリストを返す。

calcBmis :: [(Double, Double)] -> [Double]
calcBmis xs =  [bmi w h | (w, h) <- xs]
    where bmi weight height = weight / height ^ (2 :: Integer)

※ {-# OPTIONS -Wall -Werror #-} を指定していると型を明示的に (2 :: Int) としないと次のような警告がでる。型がわからないと言っているようだが、詳しくは後で調査することにする。

Warning:
Defaulting the following constraint(s) to type ‘Integer’
 (Num b0) arising from the literal ‘2’ at ch3.hs:67:49
 (Integral b0) arising from a use of^’ at ch3.hs:67:47
 In the second argument of ‘(^)’, namely ‘2’
 In the second argument of ‘(/)’, namely ‘height ^ 2’
 In the expression: weight / height ^ 2

let It Be

let式は

  • どこでも変数を束縛できる
  • let 自身も式になる
  • 局所的でガード間では共有できない
  • パターンマッチが使える

let式の構文

let 変数の束縛 in
  • let式の例
 4 * (let a = 9 in a + 1) + 2  -- 出力:42
 [let square x = x * x in (square 5, suqre 3, square 2)] -- 出力:[(25,9,4)]
 -- セミコロン(;)が使える
 let a = 100; b = 200; c = 300 in a * b * c -- 出力:6000000
 -- パターンマッチ
 (let (a, b, c) = (1, 2, 3) in a + b + c) * 100 --  出力:600

リスト内包表記でのlet

calcBmis' :: [(Double, Double)] -> [Double]
calcBmis' xs = [bmi | (w, h) <- xs, let bmi = w / h ^ (2 :: Int)]

(w, h) <- xs の部分をジェネレータと呼ぶ。

case式

case式は

  • 他の言語のcase文と同じような概念
  • let式と同じで、case式も式
  • パターンマッチが使える
  • 関数の引数に対するパターンマッチはcase式の構文糖衣

case式の構文

case expression of pattern1 -> result1
                   pattern2 -> result2
                   ...
describeList :: [a] -> String
describeList ls = "The list is " ++
                           case ls of [] -> "empty."
                                      [x] -> "a singleton list."
                                      xs -> "a longer list."

※ x, xsが未使用のため警告がでたので、オプションを次のように書き換えた

{-# OPTIONS -Wall -Werror -fno-warn-unused-binds -fno-warn-unused-matches #-}

余談

  • この章で分からなかった漢字
  • 顰蹙 (ひんしゅく)