関数

【Haskell】関数におけるパターンマッチ

【Haskell】関数におけるパターンマッチ

Haskellの関数実装において非常に重要なパターンマッチの使い方の基本について解説します。

関数におけるパターンマッチ

Haskellは、純粋関数型プログラミング言語のひとつです。Haskellでのプログラミングでは関数が中心となります。Haskellの関数の基本については「関数の基本」でまとめていますので参考にしてください。

Haskellの関数の実装において重要となる機能が「パターンマッチ」です。パターンマッチは、異なる引数の形に基づいて異なる定義を行うことができる非常に強力な機能です。基本的な考え方としては、関数が受け取る引数の値の形状に基づいて処理を分岐させることにあります。

パターンマッチにはいくつかの代表的なパターンがあり、それらを組み合わせて使うことで非常に幅広い問題に対処することが可能になります。そのため、代表的なパターンを把握しておくことは非常に有益です。

本記事では、関数における代表的なパターンマッチの種類について例を使いながら紹介することで、Haskellのパターンマッチの基本を紹介します。

リテラルパターン

最も基本的なパターンは、リテラルパターンといいます。リテラルパターンでは、以下例のようにリテラル(数値、文字列、論理値等)の一致でマッチングを行います。

isZeroOrOne :: (Eq a, Num a) => a -> Bool
isZeroOrOne 0 = True
isZeroOrOne 1 = True
isZeroOrOne _ = False
【関数使用結果例】
ghci> isZeroOrOne 0
True
ghci> isZeroOrOne 1
True
ghci> isZeroOrOne 2
False

上記例では、0または1であればTrue、その他の数値の場合はFalseを返却します。_はすべての値にマッチすることを示します。このようにHaskellでは、同じ関数に対して複数の定義を列挙していく記載方法をします。この記載方法のことをパターンマッチといいます。

なお、「isZeroOrOne :: (Eq a, Num a) => a -> Bool」の部分は型シグネチャという関数の引数や返却値の型を示す宣言で、この例ではInt等の数値型を引数に取り、Bool型を返却するという意味です。今後の関数でも型シグネチャの記載が出てきますが、型シグネチャが本記事の主題ではないので詳細の説明は省略します。

変数パターン

変数パターンは、受け取った引数の値を変数に設定して処理をするようなパターンです。以下の例におけるxのパターンが該当します。

sayNo :: (Eq a, Num a, Show a) => a -> String
sayNo 1 = "One"
sayNo 2 = "Two"
sayNo x = "Not 1 or 2. " ++ "This is " ++ show x ++ "."
【関数使用結果例】
ghci> sayNo 1
"One"
ghci> sayNo 2
"Two"
ghci> sayNo 3
"Not 1 or 2. This is 3."

上記例では、引数が1の場合は「"One"」、引数が2の場合は「"Two"」を表示します。そして、それ以外の数値が来た場合には、変数xにその値が設定され、その値を用いて「"Not 1 or 2. This is x"」という形式でxの部分に引数の数値が表示されます。

このパターンでは、順番に注意する必要があります。パターンマッチは上から順に適用されるために変数パターンを先頭に持ってきてしまうと他の処理が実行されなくなってしまいます。

ワイルドカードパターン

どのような値にもマッチするパターンをワイルドカードパターンといいます。リテラルパターンで紹介したisZeroOrOne関数の「_」がこのワイルドカードパターンの例になります。(以下再掲)

isZeroOrOne :: (Eq a, Num a) => a -> Bool
isZeroOrOne 0 = True
isZeroOrOne 1 = True
isZeroOrOne _ = False

_」は任意の値にマッチして、その値を捨てるという意味なので、マッチされた値が関数の定義の中で必要ない場合に特に有用です。上記例ではTrueFalseを返却するだけですので、引数の値自体は必要ありません。引数の値が必要な場合は、変数パターンを使用する必要があります。

また、ワイルドカードパターンはすべての引数にマッチするため、パターンマッチの順序としては一番最後に記載する必要がある点に注意が必要です。そうしない場合、他のパターンは実行されなくなってしまいます。

リストパターン

リストの形状にマッチさせるようなパターンをリストパターンといいます。代表的なものはHaskellのリストで提供されているhead関数です。head関数を自分で書いてみたhead'関数は以下のようになります。

head' :: [a] -> a
head' (x:xs) = x
【関数使用結果例】
ghci> head' [1, 2, 3, 4]
1
ghci> head' ["a", "b", "c", "d"]
"a"

上記では、(x:xs)の部分でリストの情報にマッチします。xは先頭要素、xsは残りの部部分を表しており、この表現はHaskellではよく見かける表現です。head'は先頭要素を返却する関数なわけなのでxを返却すれば関数として実現したい機能を表現できます。

この基本的な考え方が分かれば、先頭2要素を取ってくるような関数も以下のように書くことができます。

firstTwoItems :: [b] -> (b, b)
firstTwoItems (x:y:_) = (x, y)
【関数使用結果例】
ghci> firstTwoItems [1, 2, 3, 4]
(1,2)
ghci> firstTwoItems ["a", "b", "c", "d"]
("a","b")

上記の例では、先頭要素がxに2番目の要素がyに入りますので、それらの値を使ってタプルにして返却しています。また、残りの部分は使わないので今回はワイルドカードパターンの「_」を使って捨てています。

なお、このパターンにおいて()を取り除いてしまうと、リストのパターンと解釈されなくなってしまうので注意が必要です。

Note

Haskellにおいて、上記で紹介した「head'」のように関数名に「'」(アポストロフィまたはプライム記号と読みます)を使用するのは文法上で有効なものです。

既存の関数の変更版や改良版を示すためや補助的な関数名に使われるなど意味を持って「'」が使われることがあります。本サイトでは既存関数を自分で実装してみるということをよくしますが、その際には既存関数名に「'」を付けた関数名を使います。

タプルパターン

タプルの引数に対してマッチさせるパターンをタプルパターンと言います。この例の説明でよく出てくるのが座標に関する関数です。

以下は、2つの(x, y)のペアを受け取って足し算をベクトルの足し算をするようなaddVectors関数です。

addVectors :: (Num a, Num b) => (a, b) -> (a, b) -> (a, b)
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)
【関数使用結果例】
ghci> addVectors (1, 2) (3, 4)
(4,6)

この例では、1つ目の引数で受け取ったタプルの要素がx1y1に、2つ目の引数で受け取ったタプルがx2, y2に設定されます。あとはそれぞれのxyを足し算してタプルで返却しています。機能の内容が非常に分かりやすい表現になっていることが分かるかと思います。

なお、この例は以下のように書き換えることもできます。

addVectors' :: (Num a, Num b) => (a, b) -> (a, b) -> (a, b)
addVectors' a b = (fst a + fst b, snd a + snd b)
【関数使用結果例】
ghci> addVectors' (1, 2) (3, 4)
(4,6)

この実装では、1つ目のタプルをaに、2つ目のタプルをbに受け取っています。fstは、タプルから先頭要素を取得する関数で、sndはタプルから2番目の要素を取得する関数ですので、これにより同様のことができることが分かります。

同じことが表現できることはできるのですが、上記のタプルパターンでの記載の方が処理の内容がイメージしやすく、可読性が高いことが分かるかと思います。

コンストラクタパターン

データコンストラクタの形に対してマッチさせるパターンをコンストラクタパターンと言います。以下の簡単な例で見てみましょう。

data Shape = Circle Float | Rectangle Float Float

area :: Shape -> Float
area (Circle r) = pi * r * r
area (Rectangle w h) = w * h
【関数使用結果例】
ghci> area (Circle 5)
78.53982
ghci> area (Rectangle 2 5)
10.0

上記例では、Shapeという型を定義しており、Circle(円)またはRectangle(矩形)を表すものになっています。それぞれのデータコンストラクタでは、Circle(円)は半径(Float)、Rectangle(矩形)は、幅(Float)と高さ(Float)を引数に取るものとして定義しています。データコンストラクタの説明は今回の主題ではないので詳細は割愛します。

この場合、area関数は、半径が設定されているCircleとして「(Circle r)」や幅・高さが設定されたRectangleとして「(Rectangle w h)」を引数にとり、それぞれ面積を求めることができます。

コンストラクタパターンを使うことでデータコンストラクタの形状に合わせた関数の定義が可能です。

レコードパターン

Haskellのデータ定義では、レコード構文を使うことで項目に名称を付けることができるようになります。このレコード構文で定義したデータに対してマッチさせるパターンをレコードパターンと言います。

data Person = Person {name :: String, age :: Int} deriving (Show)

greet :: Person -> String
greet (Person {name = n}) = "Hello, " ++ n ++ "!" 
【関数使用結果例】
ghci> greet (Person {name = "Taro", age = 30})
"Hello, Taro!"

上記の例では、Personという型を定義しており、Personは名前(name :: String)と年齢(age :: Int)の情報を持っています。レコード構文の説明は今回の主題ではないので詳細は割愛します。

この場合、greet関数は、Personの値を引数にとって、Personの中の情報をもとに挨拶をする処理を実行しています。「greet (Person {name = n})のようにすることで引数のnameの値をnに設定して使用することができます。今回年齢のageは使わないので記載していません。もちろん「greet (Person {name = n, age = a})」とすれば、年齢についても関数内で扱うことができます。

レコードパターンを使うことで、レコード構文で定義したデータ型の形状に合わせた関数の定義が可能です。

ガード付きパターン

パターンを定義するときに条件に従って表記する方法をガード付きパターンといいます。正確にはパターンというよりは、パターン記載時の条件設定という方が適切ですが、分かりやすさを考慮してパターンと記載することにします。

ガード付きパターンでは、条件を記載することで処理結果を分岐させることができます。以下は、絶対値を返却するabs関数を、abs'関数として自分で実装してみた例です

abs' :: (Num a, Ord a) => a -> a
abs' x
    | x < 0     = -x
    | otherwise = x
【関数使用結果例】
ghci> abs' 10
10
ghci> abs' (-10)
10

ガード付きパターンでは、「|」を使って条件を列挙していきます。上記では、x0より小さい場合(x < 0)にはマイナスをかけて絶対値にして返却します。それ以外を表す条件は「otherwise」で記載することができ、上記ではxをそのまま返却しています。

ASパターン

引数で受けっとった部分をある変数に設定して扱いたいときには、ASパターンを使用します。以下の例で見てみましょう。

firstAndRest :: String -> String
firstAndRest "" = "Empty"
firstAndRest all@(x:xs) = "The First letter of " ++ all 
                        ++ " is " ++ [x] ++ ". The rest is " ++ xs
【関数使用結果例】
ghci> firstAndRest ""
"Empty"
ghci> firstAndRest "Haskell!"
"The First letter of Haskell! is H. The rest is askell!"

上記の例では、リストパターンのようにStringの情報を引数に取っていますが、(x:xs)にあたる部分をallという変数に設定しています。この時に設定する場合には、変数名@という表記を使用します。

xはリストの先頭、xsは残りの部分です。つまり、(x:xs)というのは入力されたString全体のことを表します。そのため、関数内でallを使用すると受けったString全体を表すわけです。

このようにASパターンをうまく利用すると指定した変数に値を設定してうまく扱うことが可能になります。

まとめ

Haskellの関数実装において非常に重要なパターンマッチの使い方の基本について解説しました。

パターンマッチは、異なる引数の形に基づいて異なる定義を行うことができる非常に強力な機能であり、Haskellの関数定義において非常に重要な役割を持っています。本記事では、パターンマッチの代表的なパターンを例に使って使用方法の基本を紹介しました。

パターンマッチは組み合わせにより非常に表現力が高くなり、関数定義の幅が広がります。ぜひ使用方法の基本を理解してもらえればと思います。