パターンマッチなんか書いたら負け (モナドがあるから)
この記事は CAMPHOR- Advent Calendar 2015 26日目の記事です.
はじめに (弁明)
個人的な意見といたしましては,別にパターンマッチを使おうが負けだとはこれっぽっちも思っておりませんし,むしろ,Haskell におけるパターンマッチは非常に強力な言語機構であり,見通しよく処理を記述できる素晴らしい機能だと思っています.記事のタイトルは,単に「id:ryota-ka は将来こういうことを言っていそうだ」と過去に言われた言葉をそのまま引用したものであり,私自身の主義主張とは何ら関わりのないものである旨をここに記しちょっとまさかり投げるのやめて痛い,痛いから!
この記事の趣旨について
Haskell は,圏論という分野から幾分か抽象化のアイディアを得ており,様々な型を Functor
や Monad
といった型クラスのインスタンスとすることで,それぞれの型に対する処理を共通の記法で記述することができます.というわけで,これらの力を借りながら,時に順当に書けばいいところを,半ば無理矢理モナドの性質利用して記述していこうかと思います.初学者の方にもこれらの魅力をなんとなく感じてもらい,Haskell に対する興味を少しでも抱いていただければと思っています.(というのはこじつけで,単に電車の中で考えていた型を雑にコードに書き下しただけです.)
fmap
を使った例
Just n
に適用すると Just (n * 2)
を返し,Nothing
に適用すると Nothing
を返す関数 f :: Maybe Int -> Maybe Int
を考えます.データ構築子に対するパターンマッチを使うと,以下のようになります.
f :: Maybe Int -> Maybe Int f (Just n) = n * 2 f Nothing = Nothing
でも,次のように fmap
を使って簡単に書くこともできます!
f :: Maybe Int -> Maybe Int f = fmap (* 2)
関数を適用する値が Just n
なのか Nothing
なのかという条件分岐が Functor
の性質の中に隠蔽されていて,非常に単純に記述することができました.簡単ですね,どんどんいきます.
maybe
を使った例
Just n
に適用すると n * 2
を返し,Nothing
に適用すると,(フォールバックとして) 0
を返す関数 g :: Maybe Int -> Int
を考えてみます.
これも,単純にパターンマッチを用いて書くと,次のようになります.
g :: Maybe Int -> Int g (Just n) = n * 2 g Nothing = 0
ですが,ここで maybe :: b -> (a -> b) -> Maybe a -> b
を使うと,以下のように書くことできます.
g :: Maybe Int -> Int g = maybe 0 (* 2)
maybe
は,第3引数に与えられた値が Just x
だった場合は,第2引数として与えられた関数を x
に適用し値を返し,Nothing
だった場合には,第1引数に与えられた値を返す関数です.
Either モナドを使った例
引数として整数 n
を受け取り,n
が3の倍数であれば "Fizz"
,5の倍数であれば "Buzz"
,3の倍数かつ5の倍数であれば "FizzBuzz"
,それ以外であれば,n
を文字列にしたものを返す関数 FizzBuzz
を考えてみましょう.
仮に JavaScript で書いてみると,以下のような関数です.
const fizzbuzz = (n) => { if (n % 15 === 0) { return "FizzBuzz"; } else if (n % 3 === 0) { return "Fizz"; } else if (n % 5 === 0) { return "Buzz"; } else { return n.toString(); } };
さて,この関数を Haskell で書くとき,普通に guard を使って書けば,以下のようになるかと思います.
fizzbuzz :: Int -> String fizzbuzz n | n `mod` 15 == 0 = "FizzBuzz" | n `mod` 3 == 0 = "Fizz" | n `mod` 5 == 0 = "Buzz" | otherwise = show n
見た目にわかりやすくてよいですね.では,この関数を無理矢理モナドの力を借りて表現してみましょう.
(fizzbuzz n
の n
の部分パターンマッチじゃんって思った人は fizzbuzz = \n -> either ...
って読んでね.)
fizzbuzz :: Int -> String fizzbuzz n = either id show . foldl (>>=) (return n) . map (\(pred, x) -> \n -> if pred n then (Left x) else (Right n)) $ [ (\n -> n `mod` 15 == 0, "FizzBuzz") , (\n -> n `mod` 3 == 0, "Fizz" ) , (\n -> n `mod` 5 == 0, "Buzz" ) ]
ここでは,Either a
がモナドであることを利用しています.fizzbuzz
の引数である n
は,Right
にくるんで foldl
の第2引数として渡しています.
この引数 n
が,最後の3つのラムダ式で書かれた条件にマッチするかを順に検証しきます.まず,1つめの条件が True
であれば,条件に対応する値である "FizzBuzz"
を Left
に入れて返し,False
であれば,Right n
を返します.ここで,Left x >>= _ = Left x
なので,Left "FizzBuzz"
の場合はこれ以上条件の検証を続けず,either id show
の引数として渡されることになります.また,Right n
の場合は,2番目のラムダ式の引数として n
が渡され,条件の検証が続いていきます.
foldl
以下の部分は Either String Int
型の値を返すので,Left String
の場合は何もせず(id
を適用し),Right Int
の場合は show
を適用して,どちらにしても String
を返すようにしています.このようにして,「複数の条件を順番に検証し,適合した時点で値を返す」という手続き型ライクな処理を記述することができました!
おわりに
無理しないでわかりやすいコードを書いていこうな!!
CAMPHOR- Advent Calendar 延長戦,明日27日目は @kakenn の記事です.お楽しみに!