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

do構文とMonad

どうもこんばんは、south37です!今日は前回の予告通り、do構文とMonadの関係について見て行きたいと思います!!

do構文

さて、do構文とは何だったか覚えているでしょうか?

確か、以前はIOの文脈で do構文 を紹介したと思います。

main = do
  putStrLn "Hello, what's your name?"
  name <- getLine
  putStrLn ("Hi " ++ name ++ "!")

上記のコードをコンパイルして実行すると、putStrLn関数の働きで"Hello, ~"という文字列が出力された後、標準入力で名前を受け取ってnameという変数に保存し、"Hi (入力された名前)"という文字列が出力されるのでした。

この時、一行一行の処理を糊付けして一つにまとめているのが、 do構文 が果たしている重要な役割です。

では、このdo構文がMonadとどう関係してくるのでしょうか?

Monad

ここでHaskellにおけるMonadについても復習しておきましょう。前々回、モナドは自己関手で〜 とかいろいろ言ったかと思いますが、ぶっちゃけそれは覚えてなくても問題ありませんw。重要なのは、型クラスとして定義されているMonadにおいては、return>>=(バインドと読む)という関数が定義されているという事です。

例として、Maybeにおいて、上記の関数がどういう風に実装されているかを見てみましょう。

instance Monad Maybe where
  return x = Just x

  Nothing >>= f = Nothing
  Just x  >>= f = f x

instance (型クラス名) (型名) where ~という構文は、ある型を特定の型クラスのインスタンスにする時にとられる構文で、where以降に定義が義務づけられている関数の実装を書きます。(細かい事言うとMaybeは型では無くて、型引数を一つとる コンストラクタ です。型コンストラクタも型クラスのインスタンスになるのです。)

上記のコードから、returnは値を一つ受け取ってそれをJustにくるんだ値を返す振る舞いをしている事が分かります。型で書けば、a -> Maybe aとなっています。

一方、>>=は中置関数(二つの引数の間に置く関数。+などと同様)として定義されており、Maybe a型の値(NothingまたはJust x)とa -> Maybe b型の関数であるfを受け取って、Maybe b型の値を返す振る舞いをしています。型で表せば、Maybe a -> (a -> Maybe b) -> Maybe bとなっています。

>>=は、Monadにくるまれた値を普通の関数(ただし出力はモナド)に 食わす 役割をします。

実は、この>>=がdo構文と関係してきます。

>>=でdo構文を表現する

最初に、次のようなコードを考えてみましょう。

foo :: Maybe String
(Just 3) >>= (¥x -> Just (show x ++ "!"))

¥x -> (関数の実装)というのはその場で無名関数を定義する為の構文です。xを引数として受け取り、出力はshow xxの文字列表現を生成し、"!"と連結してからJustでくるんだものとなっています。

Just 3Num a => Maybe a型の値で、それを>>=関数を使って¥x -> Just (show x ++ "!")という「a型の値を受け取ってMaybe String型の値を返す関数」に食わせている事が分かります。出力は、"3!"といった感じでしょうか。まあ、>>=の機能を考えれば普通の振る舞いですね。

では、次の様に書くとどうでしょうか。

Just 3 >>= (¥x -> Just "!" >>= (¥y -> Just (show x ++ y)))

パッと見だとよく分かりませんね...じっと見ていると、無名関数が入れ子になっている事が分かります。¥x -> ~というのはxを引数として受け取って何らかの処理を行う関数で、その処理というのが¥y -> ~という無名関数に>>=を使ってJust "!"を食わせる事です。更に、¥x -> ~>>=を使ってJust 3に対して適用されています。

このコードは、次の様に書き換える事も出来ます。

foo :: Maybe String
foo = Just 3   >>= (¥x ->
      Just "!" >>= (¥y ->
      Just (show x ++ y)))

fooという変数は何となくノリで出しただけなので気にしないでください。

重要なのは、ただ改行を入れただけで処理自体は何も変化していない事です。でも、こう書くと何となくJust 33¥xに結びついて、Just "!""!"¥yに結びついて、それが最後の行で使われている様に見えませんか?

実は実際にそのように解釈する事が出来ます。do構文を用いると、それが顕著になります。

foo = do
  x <- Just 3
  y <- Just "!"
  Just (show x ++ y)

このdo構文を用いたコードは、上で書いたコードと全く同じ処理をしています。つまり、do構文というのは>>=関数を使った処理を、より簡単に書けるようにした 糖衣構文 に過ぎなかった訳です!!

ここではMaybeについて説明をしましたが、IOについても同様に考える事が出来ます。すなわち、

main = do
  name <- getLine
  putStrLn ("Hi " ++ name ++ "!")

というコードは、

main = getLine >>= (¥name ->
       putStrLn ("Hi " ++ name ++ "!"))

というコードと同じ事をしているという訳です。getLineIO String型の値を返し、その値が>>=によって¥name -> (putStrLn ~)というString -> IO ()型の関数に適用されています。

ちなみに、IOにおける>>=の実装は、そこそこ公式っぽいページを見ても

m >>= k = ... -- executes the I/O action m and binds the value to k's input

としか載っていませんでした。IOは副作用を伴う概念である為、その関数はコンパイラレベルで実装されておりコードには落とし込めないのだと思います。ただ、変数へのバインドを行う点は、Maybeと共通しているはずです。この役割の為に、>>=はバインドと呼ばれるのです。

>>関数

>>=によってdo構文におけるx <- (Monad)という表現が理解出来る事が分かりました。では、

main = do
  putStrLn "Hi!"
  putStrLn "My name is Taro!"

の様な、独立した一行一行がdo構文でまとめられているケースはどう理解したら良いのでしょうか?

実は、これは>>関数を用いる場合の糖衣構文です。>>>>=を使って次の様に定義されています。

(>>) :: (Monad m) => m a -> m b -> m b
m >> n = m >>= ¥_ -> n

>>は二つのモナドmnを受け取り、後者のモナドnを返します。実装としては、¥_ -> nという引数を受け取るが何もせずにnを返す無名関数に対して、>>=を用いてmを食わせます。mの値が必要無い時に使われるのがこちらです。

IOの例で言えば、>>=はI/Oアクションを実行してその結果を関数に食わせる役割を果たすため、

main = putStrLn "Hi!" >> (putStrLn "My name is taro!")

と書けば、putStrLn "Hi!"の返す値は捨ててI/Oアクションだけを実行する事が出来ます。求めていた機能と一致していますね!!!

まとめ

という事で、do構文が>>=>>を用いた場合の糖衣構文に過ぎない事が分かりました。やったね!!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!