はじめに
少し昔の記事になりますが、こちらの記事で「関数型プログラミング」について紹介されています。
そこではお題として、「唐揚げ弁当をつまみ食いする」の実装があげられていました。ここにそれを再掲させていただくと、
課題:
唐揚げ弁当がいくつかあるとします。それぞれ唐揚げが複数入っています。
この中からx個の唐揚げをつまみ食いするプログラムを作りましょう。
つまみ食いはバレないようにするために、
その時点で最も唐揚げ数が多いお弁当から取るという仕様にします。
となります。
すでに手続き型言語Pascal での実装と、動的型付関数型言語Elixir 、論理型プログラミングPrologでの実装をしてみたのですが、やはりHaskellでも実装してみたくてやってみました。
実はHaskellでの実装はすでにこちらの記事ですでに行われていて、リストを使って非常に綺麗な作りになっています。ここでは少し切り口を変えて、元記事のように、お弁当に対してデータモデルを用意する方法で試してみます。
実装
まず、お弁当のデータモデルと、その表示処理を定義します。
data Bentoo = Bentoo { bentooDish :: String, bentooNum :: Int }
instance Show Bentoo where
show (Bentoo dish num) = dish ++ (show num) ++ "個"
そして、一番唐揚げがたくさん入っているお弁当を選ぶ(お弁当のインデックスを見つける)関数を用意します。
selectMaxBentoo :: [Bentoo] -> Int
selectMaxBentoo [x] = 0
selectMaxBentoo (x:xs) = n where
n = if (bentooNum x) >= (bentooNum $ xs !! (selectMaxBentoo xs)) then 0 else (selectMaxBentoo xs)+1
これを実装してて思ったのですが、関数型プログラミングでは、リストの要素に一意的なIDとかは普通付けないので、どのお弁当が唐揚げが最大かを示すのに、お弁当そのものを戻り値にするのではなく、お弁当の配列のインデックスにしないといけないですね。これはPrologのときもそうでした。Elixirのときは、お弁当をサーバーにしてしまったのでお弁当にpidというユニークIDがあったのでそれを戻り値にすることが出来たので、あまり考えていなかったのですが。
この辺りの話は、元記事でもコメントで「選ぶ」という操作が関数型っぽくないと書かれていたのですが、その意味がようやく分かった気がします。また、同じHaskellでもこちらの記事では、唐揚げが最大のお弁当を処理するのにbreak
を使うことでリストを二つに分けて、その分け目にあるお弁当だけ唐揚げを1つ減らした状態にするという、関数型らしい作りになっているように思います。
また、つまみぐいは次のように定義しました。
tsumamigui :: Int -> [Bentoo] -> [Bentoo]
tsumamigui n (x@(Bentoo dish karaage):xs) = case n of
0 -> (Bentoo dish (karaage-1)) : xs
_ -> x : (tsumamigui (n-1) xs)
Haskellは、パターンマッチが強力で、@を使ったマッチと後方参照ができるので、ちょっと楽ですね。
これらを組み合わせて、メイン処理はつぎのようになりました。
main :: IO ()
main = do
let order = [Bentoo "唐揚げ" 10, Bentoo "唐揚げ" 8, Bentoo "唐揚げ" 6]
let maxIndex = selectMaxBentoo order
let order2 = tsumamigui maxIndex order
let update order = tsumamigui maxIndex order where maxIndex = selectMaxBentoo order
foldM_ (\order -> \_ -> do
let order' = tsumamigui maxIndex order
where maxIndex = selectMaxBentoo order
putStrLn "------"
mapM putStrLn $ show <$> order'
return order') order [1..5]
コード全体では、次のようになります。
import Control.Monad
data Bentoo = Bentoo { bentooDish :: String, bentooNum :: Int }
instance Show Bentoo where
show (Bentoo dish num) = dish ++ (show num) ++ "個"
selectMaxBentoo :: [Bentoo] -> Int
selectMaxBentoo [x] = 0
selectMaxBentoo (x:xs) = n where
n = if (bentooNum x) >= (bentooNum $ xs !! (selectMaxBentoo xs)) then 0 else (selectMaxBentoo xs)+1
tsumamigui :: Int -> [Bentoo] -> [Bentoo]
tsumamigui n (x@(Bentoo dish karaage):xs) = case n of
0 -> (Bentoo dish (karaage-1)) : xs
_ -> x : (tsumamigui (n-1) xs)
main :: IO ()
main = do
let order = [Bentoo "唐揚げ" 10, Bentoo "唐揚げ" 8, Bentoo "唐揚げ" 6]
let maxIndex = selectMaxBentoo order
let order2 = tsumamigui maxIndex order
let update order = tsumamigui maxIndex order where maxIndex = selectMaxBentoo order
foldM_ (\order -> \_ -> do
let order' = tsumamigui maxIndex order
where maxIndex = selectMaxBentoo order
putStrLn "------"
mapM putStrLn $ show <$> order'
return order') order [1..5]
この結果は次のようになります。
------
唐揚げ9個
唐揚げ8個
唐揚げ6個
------
唐揚げ8個
唐揚げ8個
唐揚げ6個
------
唐揚げ7個
唐揚げ8個
唐揚げ6個
------
唐揚げ7個
唐揚げ7個
唐揚げ6個
------
唐揚げ6個
唐揚げ7個
唐揚げ6個
感想
先にも書きましたが、Haskellはパターンマッチが強力なので頼もしいです。コードも比較的見やすい気がします。まだ全体的に手続き型っぽい構造になってしまっているので、もっと練習して、良いコードが書けるようになりたいと思います。
閑話休題:この課題を続けていて、唐揚げ弁当が無性に食べたくなって、お弁当屋さんで買ってきたのですが、唐揚げは4つしかありませんでした。現実は厳しいです。