IOモナドが倒せない

色々入門書を読んでみたのだが、IOモナドというものがどういうものなのか分からない。
使い方は分かるんだが、なんでこうなってるのとか、どういう風に実装されてるのかとかさっぱり理解の外である。

ということで、まず、NiseIOモナドを自分で作って見てある程度自分で考えてから実装を見ないと損した気分になるので、考えてみた。

よくある入門書いわくIOモナドは副作用実行前の世界と副作用実行後の世界+副作用の結果を考えることで純粋になってるらしい。
だが、まあ普通に考えてそういうインターフェースは

data Env = Env {input::String, output::String }
data NiseIOResult a = NiseIOResult a Env

getChar_1 :: Env -> NiseIOResult Char
getChar_1 (Env (x:xs) y) = NiseIOResult x (Env xs y)

putChar_1 :: Char -> Env -> NiseIOResult ()
putChar_1 c (Env x y) = NiseIOResult () (Env x (c:y))

example_echo env =
  let NiseIOResult c  env'  = getChar_1 env in
  let NiseIOResult () env'' = putChar_1 c env' in
  example_echo env''

のように、副作用が環境を受け取って、新しい環境を返すようになってるものだと思う。
というか、副作用のある言語の意味論はこういう形に変換してから考えるものだったと思う。

まあこんな風に環境を陽に記述するようなやり方だと、明らかに環境が正しくリンクしてることを保障できないので論外なわけで、
これを正しくリンクさせるように制限したインターフェースがIOモナドだと思ってしまっていた。

これを元にしてNiseIOモナドを作ってみると

type NiseIO a = Env -> NiseIOResult a

instance Monad NiseIO where
  x >>= f = \env ->
    let (NiseIOResult v env') = x env in
      f v env'
  return x = \env -> NiseIOResult env x

getChar_2 :: NiseIO Char
getChar_2 = getChar_1

putChar_2 :: Char -> NiseIO ()
putChar_2 c = putChar_1 c

execute :: Env -> NiseIO a -> IO (NiseIOResult a)
execute env f =
  return (f env)

ってなったんだけど、これはなんだかおかしい。execute側がgetCharとかを実行してエミュレートしないといけないのにいれる場所がない。
だいたいこれStateモナドの劣化版じゃん。なんか作りたいものと違う。

作りたいのは

niseGetChar :: NiseIO Char
nisePutChar :: Char -> NiseIO ()
niseMain :: NiseIO ()
niseMain =
  do c <- niseGetChar
     nisePutChar c
     niseMain

executeNiseIO :: NiseIO a -> IO a

main = executeNiseIO niseMain

なインターフェースで、偽アクションは無限リストみたいになってて

executeNiseIO 偽アクション
executeNiseIO (最初の偽アクション:残りの偽アクション)

ここで最初の偽アクションを実行する

executeNiseIO (最初の偽アクション:二つ目の偽アクション:残りの偽アクション)

ここで二つ目の偽アクションを実行する

みたいになってないと、Haskellの処理系は実装できないと思うのですよ。

そうやって考え抜いてできたのがこれ

data NiseIO a = NiseIOReturn a
  | NiseIOGetChar (Char -> NiseIO a)
  | NiseIOPutChar Char (() -> NiseIO a)

instance Monad NiseIO where
  return x = NiseIOReturn x

  NiseIOReturn x >>= f = f x
  NiseIOGetChar g >>= f = NiseIOGetChar (\x -> g x >>= f)
  NiseIOPutChar ch g >>= f = NiseIOPutChar ch (\x -> g x >>= f)

niseGetChar :: NiseIO Char
niseGetChar = NiseIOGetChar NiseIOReturn

nisePutChar :: Char -> NiseIO ()
nisePutChar ch = NiseIOPutChar ch NiseIOReturn

niseMain :: NiseIO ()
niseMain =
  do c <- niseGetChar
     nisePutChar c
     niseMain


executeNiseIO :: NiseIO a -> IO a
executeNiseIO (NiseIOReturn x) = return x
executeNiseIO (NiseIOGetChar f) =
  do c <- getChar
     executeNiseIO (f c)
executeNiseIO (NiseIOPutChar c f) =
  do putChar c
     executeNiseIO (f ())

main = executeNiseIO niseMain

これならIOモナドの実装に近づけてると思える。多分