人気ブログランキング | 話題のタグを見る

48時間でSchemeを書こう/変数と代入 (4)

48時間でSchemeを書こう/変数と代入』のエラー処理の仕組みが分かったのでいよいよ変数と代入に関連する関数を見ることができる。この章で記述されているそれらの関数を概観すると次のようになる。
isBound
変数名が変数リスとに登録されているかどうかを判断する関数
getVar
変数の現在の値をとりだす関数
setVar
変数に値をセットする関数
defineVar
Scheme の define を実行する関数
bindVars
ユーザー定義の関数の実効に関する関数?
名前を見るとなんとなく関数の機能がわかる。

それでは、まず、isBound から眺めてみよう。

isBound :: Env -> String -> IO Bool
isBound envRef var = readIORef envRef >>= return . maybe False (const True) . lookup var

isBound の型をみると、Env型とString型の引数をとり、IO Bool 型の戻り値を返す。戻り値が IO a 型だから、この間数は IO モナド型の関数だ。

readIORef envRef で環境の変数テーブルを取り出し、それを lookup var に渡している。lookup は環境(変数テーブル)に変数名 var があるかどうかを検索し結果を Maybe モナド値として maybe 関数に渡す。maybe は lookup から渡された値が Nothing なら True を返し、Just x なら x に関数 (const True) を関数適用させた値を返す。この場合は const True だから、True が返る。最後に return でこの Bool 値を IO で包んで最終的な戻り値にしている。

関数 maybe の型は次のようになる。

Prelude> :t maybe
maybe :: b -> (a -> b) -> Maybe a -> b

第3引き数が、Nothing のとき第1引き数を返し、Just x の時は、x に第2引き数の関数を関数適用する。実行例は次のようになる。

Prelude> maybe 1 (*2) Nothing
1
Prelude> maybe 1 (*2) (Just 2)
4

Maybe モナド型の関数を記述するときにパターンで定義したり、if 文を使わなくてもいいので便利だ。

つぎは、getVar 関数だ。プログラムは次のようになる。

getVar :: Env -> String -> IOThrowsError LispVal
getVar envRef var  =  do
  env <- liftIO $ readIORef envRef
  maybe (throwError $ UnboundVar "Getting an unbound variable: " var)
    (liftIO . readIORef)
    (lookup var env)

getVar 関数の型を見ると、Env型 (環境)とString型の引き数をとり、IOTrowsError LispVal 型の戻値を返す。したがって、環境のリストenvRef の中から変数名が var であるものを検索して、合致するものがあればその値を返し(liftIO . readIORef)、合致するものがなければエラーの例外を発生させる (throwError)。

liftIO という関数は Control.Monad.Trans に記述されており、型は次のようになる。

Prelude Control.Monad.Trans> :t liftIO
liftIO :: (MonadIO m) => IO a -> m a

これは IO a 型の値を、モナド型 m a に変換してくれる便利なものだ。上の例では、readIORef は IO (IORef a) の型の戻値を返すが、liftIO を適用することによって IOTrowsError 型モナドの値に変換している。そこで、listing8.hs をロードして試してみた。

Prelude> :set -XExistentialQuantification
Prelude> :l listing8.hs
Ok, modules loaded: Main.
*Main> :t liftIO . readIORef
liftIO . readIORef :: (MonadIO m) => IORef a -> m a

liftIO でモナド変換できるモナド m は MonadIO クラスのインスタンスでなくてはならない。つまり、IOモナドとの複合モナドである必要があるようだ。

getVar の次は、setVar だが、処理は getVar とだいたい同じだ、変数の値を書きこむのに writeIORef が使われている。setVar はエラーがないときは LispVar を戻す。setVar は本文のコードをみればわかるので、型だけを次に示す。

setVar :: Env -> String -> LispVal -> IOThrowsError LispVal

次は defineVarだが、型は次のようになる。

defineVar :: Env -> String -> LispVal -> IOThrowsError LispVal

setVar と型が同じだが、setVar が変数名がないときはエラーになるのに対し、defineVar では変数名が環境のリストにないときは新しく作ってリストに連結する。

最後に bindVars だが、型は次のようになる。

bindVars :: Env -> [(String, LispVal)] -> IO Env

型から分かる通り、変数名と値のリストを一気に環境リストに連結する関数だ。

仕上げは評価関数 eval と main 関数の改造だが省略する。

これまでの編集はhttp://jonathan.tang.name/files/scheme_in_48/code/listing8.hs に反映されているので、:load して実行してみた。

Prelude> :set -XExistentialQuantification
Prelude> :l listing8.hs
Ok, modules loaded: Main.
*Main> :main
Lisp>>> (define a 2)
2
Lisp>>> (+ (* a 3) 5)
11
Lisp>>> quit

listing8.hs は358行だ。たったこれだけで、関数の定義以外のSchemeのかなりの部分を動作させてしまう。

今回の記事は複合モナド関連だったので、正直しんどかった。まだ、しっかりとは把握できていない。複合モナドについてもう少し要素的な実例があればうれしい。ただ、このチュートリアルのお陰で、IOモナドを扱いながら Error モナドを活用していくというような、入出力でよく出くわすであろう状況に対する対処の仕方のヒントをもらったような気がする。

今更ながら、熟練したHaskellerの技量に感銘をうけた。Haskell はとりつきは簡単なようで奥が深い。
by tnomura9 | 2011-12-30 09:44 | Haskell | Comments(0)
<< 48時間でSchemeを書こう... 48時間でSchemeを書こう... >>