Haskell 勉強メモ4

関数を書くための構文

パターンマッチ

Scala でもたくさん使うパターンマッチ。Haskell版も見てく。

渡された数が7かどうかを判別する関数。

Haskell

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

*Main> lucky 8
"Sorry, you're out of luck, pal!"
*Main> lucky 7
"LUCKY NUMBER SEVEN!"

lucky を呼ぶと、パターンが上から下の順で試される。

Scala

def lucky(n: Int): String = n match {
  case 7 => "LUCKY NUMBER SEVEN!"
  case _ => "Sorry, you're out of luck, pal!"
}

lucky(8) // val res11: String = Sorry, you're out of luck, pal!
lucky(7) // val res12: String = LUCKY NUMBER SEVEN!

階乗計算するプログラムをパターンマッチを使って、再帰的に書く。

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

Scalaの場合

def factorial(n: Int): Int = n match {
  case 0 => 1
  case k => k * factorial(k-1)
}

タプルのパターンマッチもできる。

2次元ベクトルの足し算。

addVectors :: (Double, Double) -> (Double, Double) -> (Double, Double)
addVectors a b = (fst a + fst b, snd a + snd b) -- これだと読みづらい。
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2) -- こう書くと直感的

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

*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]

-- マッチしない要素は結果に反映されない。
*Main> [x*100 + 3 | (x, 3) <- xs]
[103,403,503]
val list = Seq((1,3),(4,3),(2,4),(5,3),(5,6),(3,1))
for ((a, b) <- list) yield a + b // val res13: Seq[Int] = List(4, 7, 6, 8, 11, 4)
for ((x, 3) <- list) yield x*100 + 3 // val res14: Seq[Int] = List(103, 403, 503)

リストもパターンマッチで使える。空のリスト [] または、: を含むパターンと合致させることができる。x:xs というパターンは、リストの先頭要素を x に束縛し、リストの残りを xs に束縛する。

head' :: [a] -> a 
head' [] = error "Can't call head on an empty list, dummy!"
head' (x: _) = x

*Main> head' []
*** Exception: Can't call head on an empty list, dummy!
CallStack (from HasCallStack):
  error, called at baby.hs:40:12 in main:Main
*Main> head' [2,3,4]
2

Scalaのパターンマッチの場合は下記。

def head[T](list: List[T]): T = list match {
  case x::_ => x
  case Nil => throw new IllegalArgumentException
}

head(List(2,3,4)) // val res0: Int = 2
head(List()) // java.lang.IllegalArgumentException

asパターン

Haskellには as パターン という特殊なパターンがある。値をパターンに分解しつつ、パターンマッチの対象になった値自体も参照したいときに使う。@ を使うことで、元のリスト全体にアクセスすることができる。

firstLetter :: String -> String
firstLetter "" = "Empty string whoops!"
firstLetter all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]

*Main> firstLetter "Dracula"
"The first letter of Dracula is D"

知らなかったが Scala でも同じことができるらしい。参考

上と似たことをScalaでやると。

def firstLetter(letters: List[Char]): String = {
  letters match {
    case Nil => "Empty string whoops!"
    case all @ (x::xs) => s"The first letter of ${all} is $x"
  }
}

firstLetter("Dracula".toList) // val res3: String = The first letter of List(D, r, a, c, u, l, a) is D

@ を使うことで、元の要素にアクセスできていることが分かる。Scalaの場合は、元の要素をそのまま使えばいいんじゃとか思うけど、何か違うのだろうか。。?

ガード

引数の値が満たす性質で場合分けするとき。

bmiTell :: Double -> String
bmiTell bmi
  | bmi <= 18.5 = "underweight."
  | bmi <= 25.0 = "normal"
  | bmi <= 30.0 = "fat"
  | otherwise = "Are you Whale?"

参考本のサンプルはもっと毒々しかったけど、書くの面倒だった。bmi の値で場合分けしている。

Scalaで書く場合は

def bmiTell(bmi: Double): String =
  bmi match {
    case v if v <= 18.5 => "underweight"
    case v if v <= 25.0 => "normal"
    case v if v <= 30.0 => "fat"
    case v              => "Are you whale?"
  }

複数引数でも可能。

bmiTell :: Double -> Double -> String
bmiTell weight height
  | weight / height ^ 2 <= 18.5 = "underweight."
  | weight / height ^ 2 <= 25.0 = "normal"
  | weight / height ^ 2 <= 30.0 = "fat"
  | otherwise = "Are you Whale?"

where

命令型のプログラミング言語なら、同じ値を何度を計算するのを避けるため、変数に格納するやることが一般的。Haskellでは、where キーワードをつかて計算の中間結果に名前をつける。さっきの bmiTell だと where /height ^ 2 を何度も書く&再評価するのは無駄が多い。下記のように修正する。

bmiTell :: Double -> Double -> String
bmiTell weight height
  | bmi <= 18.5 = "underweight."
  | bmi <= 25.0 = "normal"
  | bmi <= 30.0 = "fat"
  | otherwise = "Are you Whale?"
  where
    bmi = weight / height ^ 2

where のなかでパターンマッチを使うこともできる。

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

where ブロック内では、定数だけでなく関数も定義できる。

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