Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
23
Help us understand the problem. What is going on with this article?
@lotz

Wizardモノイドとその仕組み

More than 3 years have passed since last update.

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 を取り出して実行するという処理を行っています。これで「聞く」と「表示する」の処理を入れ子にする流れを追うことができましたね。

23
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
lotz
新しい記事は Zenn <https://zenn.dev/lotz> に書いています
folio-sec
誰もがかんたんに資産運用することができるサービス「フォリオ」を作っているFinTech系スタートアップ

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
23
Help us understand the problem. What is going on with this article?