GHC 8.0からIO型がMonoidのインスタンスになりました。これを利用した便利なテクニックを、Gabriel GonzalezさんがThe wizard monoidとしてブログで紹介されています。
Wizardの意味は魔法使いですが、ソフトウェアをインストールする時とかに出てくるインストールウィザードのような対話形式で作業を誘導してくれるソフトウェアのことでもあります。WizardモノイドのWizardは後者の意味で使われています。
Wizardモノイド
まず元の記事の流れに沿ってWizardモノイドを紹介したいと思います。以下のような例を考えてみましょう。
main :: IO ()
main = do
-- 最初に全ての情報を入力してもらう
putStrLn "名前は?"
name <- getLine
putStrLn "年齢は?"
age <- getLine
-- 最後に全てのアクションを実行する
putStrLn ("名前: " ++ name)
putStrLn ("年齢: " ++ age)
この短いプログラムには
- 名前を聞いて最後に名前を表示する
- 年齢を聞いて最後に年齢を表示する
という明らかなパターンがありますが、それぞれの「聞く」と「表示する」が入れ子になっているせいでうまく分割できません。そこで登場するのがWizardモノイドです。Wizardモノイドを使うと以下のように書けます。
import Data.Monoid ((<>))
name :: IO (IO ())
name = do
putStrLn "名前は?"
x <- getLine
return (putStrLn ("名前: " ++ x))
age :: IO (IO ())
age = do
putStrLn "年齢は?"
x <- getLine
return (putStrLn ("年齢: " ++ x))
runWizard :: IO (IO a) -> IO a
runWizard request = do
respond <- request
respond
main :: IO ()
main = runWizard (name <> age)
名前と年齢それぞれの処理をうまく分離することが出来ています。どの部分がWizardモノイドになっているのかを敢えて型で定義すると以下のようになると思います。
type Wizard a = IO (IO a)
Wizardモノイドによってコードを分割したことで処理をまとめることが出来るようになりました。
import Data.Monoid ((<>))
prompt :: String -> IO (IO ())
prompt attribute = do
putStrLn (attribute ++ "は?")
x <- getLine
return (putStrLn (attribute ++ ": " ++ x))
runWizard :: IO (IO a) -> IO a
runWizard request = do
respond <- request
respond
main :: IO ()
main = runWizard (prompt "名前" <> prompt "年齢")
もし追加で好きな色を尋ねたくなったら以下のように修正すればいいだけですね。
main :: IO ()
main = runWizard (prompt "名前" <> prompt "年齢" <> prompt "好きな色")
入力→出力のパターンだけでなく、アクション→アクションのパターンなら何でも入れ子に合成できるのでWizardモノイドは意外と使い道が広くて重宝しそうです。
Wizardモノイドの仕組み
それではなぜWizardモノイドがIOの入れ子の合成を実現できているのか見ていきましょう。その秘密はIOのMonoidのインスタンスにあります。IOのMonoidのインスタンスは以下のように定義されています。
instance Monoid a => Monoid (IO a) where
mempty = pure mempty
mappend = liftA2 mappend
liftA2
は
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
と定義されている関数です。mappend
の定義を分かりやすく書き下すと以下のようになります。
mappend action1 action2 = do
result1 <- action1
result2 <- action2
pure (result1 <> result2)
要はアクションを一つずつ実行して得られた結果を結果の型のMonoidインスタンスのmappend
で合成するという処理になります。これを踏まえると先の例の name <> age
は以下のような処理になっているはずです。
do
response1 <- name
response2 <- age
pure (response1 <> response2)
name
と age
の実装を思い出すと response1
と response2
はそれぞれ名前と年齢を表示する処理になっているはずです。
runWizard
は name <> age
を実行したあとさらに最後の response1 <> response2
を取り出して実行するという処理を行っています。これで「聞く」と「表示する」の処理を入れ子にする流れを追うことができましたね。