38
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Wizardモノイドとその仕組み

Last updated at Posted at 2018-02-10

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)

nameage の実装を思い出すと response1response2 はそれぞれ名前と年齢を表示する処理になっているはずです。

runWizardname <> age を実行したあとさらに最後の response1 <> response2 を取り出して実行するという処理を行っています。これで「聞く」と「表示する」の処理を入れ子にする流れを追うことができましたね。

38
22
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?