do 記法についてのまとめ。

haskell の do 記法についてまとめてみました。
haskell 本でそれまで関数プログラミングスタイルな話が続いていたのに do 記法が出てきた途端関数プログラミングっぽくなくなってしまったので。

具体例から考えてみます。以下のコードは一体何をやっているのか。

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

これを do を使わずに書くと

main =
  putStrLn "Hello, what your name?">>=
  (\x->getLine >>=
  (\name->putStrLn ("Wellcome!!"++name)))

となる。
なんじゃこりゃ。
とりあえず do 記法はネストした >>= の記述を簡略化したものといえそう。

これまでと同様に図にして考えてみる。
図1.

     putStrLn "Hellow .."
                  X
  String ------> IO() ---------------> IO()
                  ↑                    ||        
                ()                     ||
             +------------------------------------------+
             | IO String   ----------> IO()             |
             |     s = getLine()       ||               |
             |   ↑                     ||                |
             |  String    -----------> IO()             |
             |   name     putStrLn("Wellcome!!"++name)  |
             +------------------------------------------+

* 関数プログラミングは関数をパイプして行くことで処理が進む。
* main の型は IO() なので最終的な型は IO になっている。
を踏まえる。

最初の putStrLn "Hellow ..." は IO() の空間の何かの元 X を生成しています。
(「プログラミング haskell」 によれば IO() の () は意味をなさないアクションを表す。
IO Char の Char のかわりに () をつかっているだけ。)
処理をパイプして行かないと行けないので、この生成された X をほっとくとかはできない。
(他の言語のように途中に print を挟んで値を出力するとかできないのはそのため。)

X を何かの関数 IO() -> IO() にわたさなくてはいけない。
(-> IO() なのは main が IO() を生成する関数だから。最終的には IO() の元が生成されていないと行けない。)

X は IO() の元(意味ないアクション)なので
受け取った X を無視するような関数であればよいでしょう。

Prelude> let f x = return "unknown"
Prelude> f $ putStrLn "hoge"
"unknown"

みたいに。

続きの処理(ユーザの入力を受け取って処理する)を書きたいから f の内部にその処理を書けばよいはず。
f のかわりに >>= で作った関数でもよいよね。
ってわけで

main =
  putStrLn "Hello, what your name?">>=
 (\x -> 
    (なんかの処理))

って形になるはず。
続く getLine も IO String 空間に元 s を生成する。この s もほっとくわけに行かないのでなんかの関数 IO String -> IO() に渡さないと行けない。
これも >>= で作った関数でいいよね。

main =
  putStrLn "Hello, what your name?">>=
 (\x -> getLine
    (\name -> (なんかの処理)))

もちろん name を無視してもいいけど、使ってもよい。
最後の putStrLn は "Wellcom!!"++name を受け取って IO() 空間に元を生成している。
(これがmainの評価結果になる。)

最初 putStrLn とか getLine とかが関数言語的にどういう扱いなのかよくわからなかったけど、要はこいつらは IO() や IO String 空間に元を生成するため生成器ってわけね。(3 とか 'c' とか記述して Num 空間に 3 の元を Char 空間に 'c' の元を生成するのとおなじ)

というわけで、連続で処理をしていくときには lambda 関数がどんどんネストしていくことになる。
(図1 でいうなら下の段の左から右への矢印をどんどん上の段にリフトしていっている。)


ネストしまくると非常に見づらい。そんなときに do 記法を使う。

main=
 do
   x <- putStrLn "Hello, what your name?"
   name <- getLine
   y <- putStrLn ("Wellcome!! "++name)
   return(y)

格段の <- の右側でそれぞれの空間の元を生成して、左側にその元の生成元になりそうなものを束縛している。(使わない束縛変数は記述しなくてよい。)

ただし do は >>= の別表記であるので do の各段で m a -> m b になっていないといけないことに注意。
今の場合は IO a -> IO b (a=() または a=String, b=()) となっている。
例えば以下はコンパイルできない。

Prelude>  Just 3>>=(\x-> print $ Just(x+1))
<interactive>:12:16:
    Couldn't match expected type `Maybe b0' with actual type `IO ()'

一方こっちはできる。

Prelude>  print $  Just 3>>=(\x-> Just(x+1))
Just 4

考えてみれば当たり前で前者は

 Maybe Int ------------> IO()
    ↑                   ↑
   Int -----------------+

となっている。 >>= の条件は

 m a ---------------> m b
  ↑                   ↑
  a-----------------> b

で a -> (a->mb) を m a -> m b にシフトするものだから。


* まとめ
do 記法で関数言語っぽくなくなっているようにみえてちゃんと関数言語してました。




追記:
ネストした >>= をフラットに書くために do 記法をつかった。ネストじゃなくて>>=の連続作用も do で書ける。

print $ Just 3 >>= (\x -> Just ((show x)++"!!!")) >>= (\x -> Just( "!!!"++x))
print $ do
  x <- Just 3
  y <- Just ((show x)++"!!!")
  Just ("!!!"++y)

Just "!!!3!!!"
Just "!!!3!!!"

最初の例は

putStrLn "Hello, what your name?">>=(\x->getLine)>>=(\name->putStrLn ("Wellcome!!"++name))

のように書ける。
いずれにせよ a -> m b を m a -> m b にリフトするという操作を続けている。