Haskell③パターンマッチについて
Haskell③パターンマッチについて
前回まで、Haskellについて以下の記事を書いてきた。
▶︎ すごいHaskellたのしく学ぼう①:概要と基本構文
▶︎ Haskell②型を信じろ! データ型と型クラス、型変数
今回はHaskellのパターンマッチングについて書いていきたいと思う。
3章について 概要
Haskellの関数構文について見ていく
- 値を手軽に分割する方法 :パターンマッチング
- if/elseの連鎖を避ける方法 :ガード
- 計算の中間データを一時的に保存し複数回利用する方法 ⇦ これは次回以降で。
3-1. パターンマッチ
📚Pattern matching consists of specifying patterns to which some data should conform and then checking to see if it does and deconstructing the data according to those patterns.
データが従うべきパターンを指定し、そのパターンに従ってデータを分析するために使う
パターンマッチは、
データを特定の構造や形状と照合し、その構造に従って分解・分析する機能で、
データから必要な部分を簡単に取り出すことができるもの。
🌱 シンプルなパターンマッチの関数 ①
lucky :: (Integral a) => a -> String
lucky 7 = "lucky number 7"
lucky x = "Oh no, you're out of luck!"
関数の補足
- 型シグネチャ:
lucky :: (Integral a) => a -> String
-Integral
型クラスに属する任意の型a
を受け取り、String
型を返す- パターンマッチングによる定義:
-lucky 7 = "lucky number 7"
: 入力が7の場合に実行される定義
-lucky x = "Oh no, you're out of luck!"
7以外の任意の入力に対して実行される定義(x
は変数)
📖 Haskellのパターンマッチには大きく2つの種類がある
これを読み込んで実行してみよう。
ghci> lucky 7
"lucky number 7"
ghci> lucky 2
"Oh no, you're out of luck!"
🌱 シンプルなパターンマッチの関数 ②
sayMe :: (Integral a) => a -> String
sayMe 1 = "One!"
sayMe 2 = "Two!"
sayMe 3 = "Three!"
sayMe x = "Not between 1 and 3"
これも同様に実行してみる。
ghci> sayMe 2
"Two!"
ghci> sayMe 7
"Not between 1 and 3"
📖パターンマッチの重要ポイント
🌱 1. 重要ポイント① 上から順に評価
さっき実行したsayMe
関数の順序を変えてみよう。
何にでも当てはまってしまう変数を一番上に持ってきてみる。
sayMe :: (Integral a) => a -> String
sayMe x = "Not between 1 and 3"
sayMe 1 = "One!"
sayMe 2 = "Two!"
sayMe 3 = "Three!"
この変数を一番上に持ってきてしまうと、どんな値とでもマッチしてしまうので
後ろの処理に行けなくなってしまう。
実行する
ghci> sayMe 1
"Not between 1 and 3"
⚠️ パターンマッチを作成するときは最後に全てに合致するパターンを入れる必要がある!
🌱 2. 網羅的なパターンの重要性
すべての可能な入力を網羅していないと、実行時エラーが発生する。
では今度は、先ほどのsayMe関数の変数のマッチングを消して実行してみる.
sayMe :: (Integral a) => a -> String
sayMe 1 = "One!"
sayMe 2 = "Two!"
sayMe 3 = "Three!"
実行してみよう。
ghci> sayMe 1
"One!"
ghci> sayMe 4
"*** Exception: haskell-study.hs:(6,1)-(8,18): Non-exhaustive patterns in function sayMe
HasCallStack backtrace:
collectBacktraces, called at libraries/ghc-internal/src/GHC/Internal/Exception.hs:169:13 in ghc-internal:GHC.Internal.Exception
toExceptionWithBacktrace, called at libraries/ghc-internal/src/GHC/Internal/Exception.hs:89:42 in ghc-internal:GHC.Internal.Exception
throw, called at libraries/ghc-internal/src/GHC/Internal/Control/Exception/Base.hs:434:30 in ghc-internal:GHC.Internal.Control.Exception.Base
「Non-exhaustive patterns」(網羅的でないパターン)のExceptionが投げられる!
パターンマッチを書く際は必ず全部のパターンを網羅する必要がある。
再帰的な定義との相性
パターンマッチングと再帰を組み合わせることで、数学的定義に近い形で関数を実装できる。
Ex.階乗を求める関数
factorial :: (Integral a) => a -> a
factorial 0 = 1 -- 基底ケース(再帰の終了条件)
factorial n = n * factorial (n - 1) -- 再帰ステップ
この関数の補足(説明)
この関数が実行される流れを… factorial 3
を計算するケースで見てみましょう:
-
factorial 3
を評価します- 3は0ではないので、2番目のパターンが適用されます:
3 * factorial (3 - 1)
- 3は0ではないので、2番目のパターンが適用されます:
-
factorial 2
を評価します- 2は0ではないので:
2 * factorial (2 - 1)
- 2は0ではないので:
-
factorial 1
を評価します- 1は0ではないので:
1 * factorial (1 - 1)
- 1は0ではないので:
-
factorial 0
を評価します- 0なので、基底ケースが適用され:
1
が返されます
- 0なので、基底ケースが適用され:
- 戻りながら計算を行います:
-
factorial 1
=1 * 1
=1
-
factorial 2
=2 * 1
=2
-
factorial 3
=3 * 2
=6
-
最終的に factorial 3
は 6
という結果になる。
→ “再帰”については4章で触れるのでここでは詳しく書かない。
📚 タプルのパターンマッチ
2つの二次元空間のベクトル(ペアで表す)を受け取ってそれを足し合わせる関数をかいてみよう。
もし!!パターンマッチを使わないならば...
前回紹介した fst
とsnd
関数を使用してかける。
addVectors :: (Double, Double)->(Double, Double) -> (Double, Double)
addVectors2 a b = (fst a + fst b, snd a + snd b)
実行する
ghci> addVectors2 (1, 2) (3, 4)
(4.0, 6.0)
計算できるのだが、パターンマッチを使うとどうなるか。
addVectors :: (Double, Double)->(Double, Double) -> (Double, Double)
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)
関数の補足
- 型シグネチャ:
(Double, Double) -> (Double, Double) -> (Double, Double)
:
2つの(Double, Double)
型のタプルを受け取り、
1つの(Double, Double)
型のタプルを返すよ- パターンマッチの部分:
-addVectors (x1, y1) (x2, y2)
: ここで2つのタプルを直接分解
- 第1引数のタプルからx1
とy1
を取り出し
- 第2引数のタプルからx2
とy2
を取り出し- 関数本体:
-(x1 + x2, y1 + y2)
: 取り出した値を使って新しいタプルを作成している
- 新しいタプルの第1要素は両方のタプルの第1要素の和
- 新しいタプルの第2要素は両方のタプルの第2要素の和
実行
ghci> addVectors (1, 2) (3, 4)
(4.0, 6.0)
Haskellのパターンマッチでかくと、
fst
やsnd
のような補助関数を使わずに構造的に分解できるのが特徴で、
何が良いかというと、タプル一つのよう一つの要素に名前がつけられる!
👀 では、トリプルの時はどうする?
thirdをとる関数はデフォルトではないため、自分で定義するか
パターンマッチを使う方法がある。
🌱 自分で定義するならば...
first :: (a, b, c) -> a
first (x, _, _) = x
second :: (a, b, c) -> b
second (_, y, _) = y
third :: (a, b, c) -> c
third (_, _, z) = z
addVectors3D :: (Double, Double, Double) -> (Double, Double, Double) -> (Double, Double, Double) -> (Double, Double, Double)
addVectors3D a b c = (first a + first b + first c, second a + second b + second c, third a + third b + third c)
実行
ghci> addVectors3Dver2 (1, 2, 3) (4,5,6) (7,8,9)
(12.0,15.0,18.0)
🌱 パターンマッチを使って書くならば。。。
-- 3次元ベクトルの加算関数
addVectors3D :: (Double, Double, Double)->(Double, Double, Double) -> (Double, Double, Double)
addVectors3D (x1, y1, z1) (x2, y2, z2) = (x1 + x2, y1 + y2, z1 + z2)
ghci> addVectors3D (1,2,3) (4,5,6)
(5.0,7.0,9.0)
基本的に2要素タプルと同じ構文で行えて、違いは要素の数のみ。
Haskellでは4つ以上の要素を持つタプルも同じようにパターンマッチできるのだが…
実際には多くの要素を持つタプルより、カスタムデータ型を定義する方が良い.
📚 リスト内包表現でのパターンマッチ
リスト内包表現でもパターンマッチが使える。
Ex.タプルのリストから各タプルの要素の和を計算
ghci> let xs = [(1,3), (4,3), (2,4), (5,3), (5,6), (3,1)]
ghci> [a+b | (a,b) <- xs]
[4,7,6,8,11,4]
(a,b) <- xs
の部分でパターンマッチを行い、各タプルを a
と b
に分解している。
パターンマッチが失敗した場合(例えば3要素のタプルがあった場合)、
そのリスト要素はスキップされて次の要素に進む。
→ これは特に異なる形式のデータがある場合に便利。
Ex
ghci> let mixed = [(1,2), (5), (6,7)]
ghci> [a+b | (a,b) <- mixed]
[3,13]
この例では、(3,4,5)
は2要素のタプルではないため、
パターンマッチに失敗し、スキップされます。
結果として、(1,2)
からの 1+2=3
と (6,7)
からの 6+7=13
だけが計算される。
同様に普通のリストでも可能だ。
Ex.
head' :: [a] -> a
head' [] = error "can't call head on an empty list, dummy!"
head' (x:_) = x
- 解説
- 型シグネチャ
-[a]
: 任意の型a
の要素からなるリスト
-> a
: 同じ型a
の単一の値を返す
- パターンマッチング
- 関数本体では、2つのパターンマッチングケースが定義されている
- 1, 空リストに対しては、エラーメッセージ付きでエラーを発生させます
-error
関数はプログラムの実行を停止させる
- 2, 1つ以上の要素を持つリストの場合
-(x:_)
のパターンは、リストを先頭要素x
と残りの部分_
に分解する
実行結果
ghci> head' [1, 2, 3, 4, 5]
1
ghci> head' "Hello"
'H'
ghci> head' []
*** Exception: can't call head on an empty list, dummy!
🌱 もう少し見てみよう。
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:_) = "This list is long. The first two elements are: " ++ show x ++ " and " ++ show y
解説
- パターンマッチ
- リストが空(
[]
)の場合: 「リストは空です」とメッセージを返す。- 1要素のリストの場合:
(x:[]) は先頭要素がx
で、残りが空リスト[]
であるリストにマッチ
“The list has one element: “の文言とともにその要素を表示する。- (x:y:[]): 2要素のリストの場合:
"The list has two elements”という文言とともに両方の要素を表示する。- (x:y:_):3要素以上のリストの場合
このパターンは3要素以上の任意の長さのリストにマッチし、最初の2要素だけを表示する。
ここでのちょっとしたポイント
-
(x:[])
は[x]
と同じ。/(x:y:[])
は[x,y]
と同じ。 -
(x:y:_)
には角括弧による短縮形がない。
なぜなら角括弧記法[...]
は完全なリストを表すから。 -
(x:y:_)
は「最初の2要素がx
とy
で、その後に0個以上の要素が続くリスト」を表す。
これは「少なくとも2要素あるリスト」を意味し、具体的な長さは指定しない。
📖 安全性とパターンの網羅性
tell
関数は「安全」とされている。なぜなら:
- あらゆる可能なリストの形(空、1要素、2要素、3要素以上)に対応するパターンが定義されている
- どのようなリストが入力されても、適切なパターンにマッチして結果を返す
- パターンが網羅的(exhaustive)であるため、例外が発生する心配がない
📖 リストパターンマッチでの++演算子について
最後の注意点として、パターンマッチングでは++
演算子を使うことができません:
パターン(xs ++ ys)に対して合致させようにも、
リストのどの部分をxsに合致させて、どの部分をysに合致させればいいか、
Haskellに伝えようがありません。
この制限がある理由は:
- パターンマッチングは構造的に行われるもの。
-
(xs ++ ys)
のような表現は、リストをどこで分割すべきか一意に決定できない - たとえば
[1,2,3,4]
というリストは、[]+[1,2,3,4]
、[1]+[2,3,4]
、[1,2]+[3,4]
など、多くの異なる方法で分割できる
理論的には(xs ++ [x,y,z])
(末尾の3要素を分離)のようなパターンは
一意に決定できそうだが、
Haskellのパターンマッチングシステムはこのような合致をサポートしていない。
📚 asパターン
There's also a thing called as patterns. Those are a handy way of breaking something up according to a pattern and binding it to names whilst still keeping a reference to the whole thing.
値をパターンに分解しつつ、
パターンマッチの対象になった値自体も参照したいときに使える
データ構造の一部を分解しながら、元の構造全体も同時に参照するもの。
📖 基本構文
名前@パターン
-
名前
は、マッチした値全体を参照するための識別子 -
@
は特殊な記号で、asパターンを示す -
パターン
は通常のパターンマッチングパターン
Ex,
firstLetter :: String -> String
firstLetter "" = "Empty string, whoops!"
firstLetter all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]
解説
- パターンマッチ:
-firstLetter "" = "Empty string, whoops!"
空の文字列が入力された場合のエラーメッセージ
-firstLetter all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]
-all@(x:xs)
の部分:
(x:xs)
: 文字列を先頭文字x
と残りの部分xs
に分解
all@
: 元の文字列全体をall
として参照
実行する
ghci> firstLetter "Dracula"
"The first letter of Dracula is D"
ガード:if/elseの連鎖を避ける方法について
📚Whereas patterns are a way of making sure a value conforms to some form and deconstructing it, guards are a way of testing whether some property of a value (or several of them) are true or false. That sounds a lot like an if statement and it's very similar. The thing is that guards are a lot more readable when you have several conditions and they play really nicely with patterns.
パターンが、ある値が何らかの形に適合していることを確認し、それを分解する方法であるのに対し、ガードは、ある値(あるいはいくつかの値)のプロパティが真か偽かをテストする方法である。
ガードはHaskellで条件分岐を行うための優れた方法で、
特に複数の条件を扱う場合に威力を発揮するもの。
(Kotlinでいうwhen式に似てる!)
パターンマッチングが「値の形状」に着目するのに対し、
ガードは「値の特性や条件」をテストする。
📖 ガードの基本構文
関数名 パラメータ
| 条件1 = 結果1
| 条件2 = 結果2
| 条件3 = 結果3
| otherwise = デフォルト結果
ガードは縦線(パイプ記号 |
)で示され、それぞれ右に論理式(ブール式)を書いていく。
Ex.BMI(体格指数)に基づいてメッセージを返す関数
bmiTell :: (RealFloat a) => a -> String
bmiTell bmi
| bmi <= 18.5 = "You're underweight, you emo, you!"
| bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
解説
1. BMI値を引数として受け取ります
2. 各ガードが上から順に評価されます
3. 最初にTrueとなるガードに対応する結果が返されます
otherwise の使用
otherwise
は単にTrue
と定義されているキャッチオールガード。
- 全てのガードがFalseの場合に使用される
- パターンマッチングと組み合わせた場合、全ガードがFalseならば次のパターンへと評価が進む
ガードは複数のパラメータを取る関数でも使用できる
bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
| weight / height ^ 2 <= 18.5 = "You're underweight, you emo, you!"
| weight / height ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| weight / height ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
実行
ghci> bmiTell 85 1.90
"You're supposedly normal. Pffft, I bet you're ugly!"
🌱 シンプルな例:max関数
独自のmax関数をガードで実装する例:
max' :: (Ord a) => a -> a -> a
max' a b
| a > b = a
| otherwise = b
bmiTell :: (RealFloat a) => a -> String
bmiTell bmi =
if bmi <= 18.5 then
"You're underweight, you emo, you!"
else if bmi <= 25.0 then
"You're supposedly normal. Pffft, I bet you're ugly!"
else if bmi <= 30.0 then
"You're fat! Lose some weight, fatty!"
else
"You're a whale, congratulations!"
🌱 ガードの利点
- 可読性: 特に複数の条件がある場合、ガードは縦に整列していて読みやすい
- 意図の明確さ: 各条件と結果の関係が視覚的に明確
- 拡張性: 新しい条件を追加するのが簡単
- パターンマッチングとの組み合わせ: パターンマッチングとシームレスに連携
Discussion