読者です 読者をやめる 読者になる 読者になる

パターンマッチなんか書いたら負け (モナドがあるから)

この記事は CAMPHOR- Advent Calendar 2015 26日目の記事です.

はじめに (弁明)

個人的な意見といたしましては,別にパターンマッチを使おうが負けだとはこれっぽっちも思っておりませんし,むしろ,Haskell におけるパターンマッチは非常に強力な言語機構であり,見通しよく処理を記述できる素晴らしい機能だと思っています.記事のタイトルは,単に「id:ryota-ka は将来こういうことを言っていそうだ」と過去に言われた言葉をそのまま引用したものであり,私自身の主義主張とは何ら関わりのないものである旨をここに記しちょっとまさかり投げるのやめて痛い,痛いから!

この記事の趣旨について

Haskell は,圏論という分野から幾分か抽象化のアイディアを得ており,様々な型を FunctorMonad といった型クラスのインスタンスとすることで,それぞれの型に対する処理を共通の記法で記述することができます.というわけで,これらの力を借りながら,時に順当に書けばいいところを,半ば無理矢理モナドの性質利用して記述していこうかと思います.初学者の方にもこれらの魅力をなんとなく感じてもらい,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 nn の部分パターンマッチじゃんって思った人は 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 の記事です.お楽しみに!