例外処理とエラー・モナドについて少し予習したので、『48時間でSchemeを書こう/エラー処理と例外』が読めるようになった。
この記事で『48時間でSchemeを書こう』というチュートリアルのチュートリアル記事の最後にする。Parsec の構文解析の結果をどうプログラムの動作に結びつけていくかを知るという目的は達成されたとおもえるからだ。しかし、Haskell を使いこなすためのその他のヒントが『48時間..』のページには満載されている。管理人の Haskell 力が成長したらまた残りの部分にも挑戦してみたい。 それでは、本文を最初から読んでみよう。まずは、Control.Monad.Error のモジュールのインポートから。 import Control.Monad.Error これをやっておくと、戻値を Either でラッピングするだけで、エラー・モナド(Either モナド)型の関数を作ることができる。また、エラー・モナド型の関数には、throwError と catchError という2つの関数を関数適用できる。 次にやることは、Scheme 用のエラー値のセット LispError を作ること。 data LispError = NumArgs Integer [LispVal] ... (以下省略) Scheme の構文解析や、実行の段階でエラーが発生した時、これらのエラー値が戻される。また、エラー値を文字列に変換する関数 showError は必須。 showError :: LispError -> String showError (UnboundVar message varname) = message ++ ": " ++ varname ... (以下略) LispError を Show クラスのインスタンスにして、show を showError で定義しておくと show 関数で LispError 関数を文字列化できる。 instance Show LispError where show = showError LispError を例外発生時にエラー情報として戻すためには、LispError を Error クラスのインスタンスにして、noMsg と strMsg を定義しなくてはならない。逆にたったこれだけで、LispError をエラー情報として利用することができるようになる。 instance Error LispError where noMsg = Default "An error has occurred" strMsg = Default 次にやることは、throwError でユーザ例外を発生したり、catchError で例外を補足するときに LispError を使えるようにすること。このためには、LispError を Either でラッピングする必要がある。 type ThrowsError = Either LispError Either 型コンストラクタを利用するときは、Either a b という形を取るのに上の例では b が記述されていない。これは、型コンストラクタの定義をカリー化することで、Either LispError Integer や Either LispError LispVal など Right b のコンテナに入るデータ型が異なっていても良いようにするためだ。 ここまでで、自前のエラー値を使うためのインフラは整った。これまでの手順は、ユーザ定義エラーを使うための定型的な手順になる。新米 Haskeller が最も欲しい情報だ。 インフラはできたので、後は要所要所でエラー発生の例外を生成させるようにする。 先ず構文解析のパーサ readExpr が parse 関数で例外が発生したときに例外を投げるようにしている。 readExpr :: String -> ThrowsError LispVal readExpr input = case parse parseExpr "lisp" input of Left err -> throwError $ Parser err Right val -> return val case parse paseExpr "lisp" input of でパーサからのエラー情報 Left err を取得した場合、そのエラーを lispError 型の Parser データコンストラクタでラップして throwError 関数に渡すと、Parser err は ThrowsError 型にさらにラップされて、例外として投げられる。 一方が ThrowsError モナド値になったので、マッチが成功した時の値 Right val も return val でThrowsError モナドにして返さなければ、戻値の型を合わせられない。 この改造で readExpr はマッチが失敗した場合は、Left (Parser err) を例外として投げ、成功した時は Right val が戻値として返される。parse 関数からの Either 型の戻値は、readExpr で ThrowsError モナドの値に変換される。 こうして構文解析器からエラーが投げられるようになったが、次は評価器からもエラーが投げられるようにする。文字列や、数値や、真理値やクォーテッドリストが評価された場合は、それを return で ThrowsError モナド値にして戻す。また、関数の評価の時は引き数 args をモナドマップの mapM で評価し、それを apply func 関数に渡す。LispVal のBadFormデータ型の場合は、throwError 関数を使ってエラーを投げる。 eval :: LispVal -> ThrowsError LispVal eval val@(String _) = return val eval val@(Number _) = return val eval val@(Bool _) = return val eval (List [Atom "quote", val]) = return val eval (List (Atom func : args)) = mapM eval args >>= apply func eval badForm = throwError $ BadSpecialForm "Unrecognized special form" badForm apply 関数は func と args を引数に取り、func が primitives テーブルになければエラーを投げ、primitive テーブルにあれば、それを args に関数適用し Right LispVal の形で戻値を返す。 apply :: String -> [LispVal] -> ThrowsError LispVal apply func args = maybe (throwError $ NotFunction "Unrecognized primitive function args" func) ($ args) (lookup func primitives) 関数適用 ($ args) の時に return が用いられていないが、primitives テーブルの関数の方を ThrowsError Lispval の値を戻すように手を入れてある。 primitives :: [(String, [LispVal] -> ThrowsError LispVal)] このあたりは型チェックの厳格さに対応するための綱渡のように見える。しかし、コードを読む側に立つと、戻り値の型を追跡するだけで、どこが関数の終わりなのかを知ることが容易になる。 primitives テーブルの中に現れる次の2つの関数 numericBinop、unpackNum もエラーを投げるパターンを追加すると共に戻値を ThrowsError型になるように改変してある。 numericBinop :: (Integer -> Integer -> Integer) -> [LispVal] -> ThrowsError LispVal numericBinop op singleVal@[_] = throwError $ NumArgs 2 singleVal numericBinop op params = mapM unpackNum params >>= return . Number . foldl1 op unpackNum :: LispVal -> ThrowsError Integer unpackNum (Number n) = return n unpackNum (String n) = let parsed = reads n in if null parsed then throwError $ TypeMismatch "number" $ String n else return $ fst $ parsed !! 0 unpackNum (List [n]) = unpackNum n unpackNum notNum = throwError $ TypeMismatch "number" notNum 最後に main 関数も ThrowsError 型のモナドを扱えるように改変してある。 main :: IO () main = do args <- getArgs evaled <- return $ liftM show $ readExpr (args !! 0) >>= eval putStrLn $ extractValue $ trapError evaled IOモナドの中で、ThrowsError モナドを扱うというモナドの入れ子構造になるので、新米 Haskeller には少し荷が重かった。そこで本文の説明を引用するだけにする。 この新たな関数がやっていることは以下の通りです。
ここまでの編集結果は listing5.hs に反映されている。コピーした listing5.hs を ghci に :load してエラー処理が起きるかどうか確認してみた。 Prelude> :l listing5.hs [1 of 1] Compiling Main ( listing5.hs, interpreted ) Ok, modules loaded: Main. *Main> :main "(+ 2 \"two\")" Invalid type: expected number, found "two" チュートリアルのチュートリアルという変な記事が続いているが、『48時間でSchemeを書こう』の著者の背景知識と管理人のそれがかなりレベルに差があるので仕方がない。 しかし、きちんと動くコードが添付されているので、まず動かしてから何が書かれているのかを確かめられるのでありがたい。普通の入門書には書かれていないテクニックが満載なのがうれしい。おなじようなコードを書けと言われても書けないと断言できるが、実際に動くコードが手元にあれば、習熟した後でもう一度読んで理解を深めることができるだろう。 『48時間でSchemeを書こう』のチュートリアルのチュートリアル記事はこれでおしまい。Parsec の構文解析の結果をどのようにプログラムに結びつけていくかという部分は終了できたと思うからだ。さらに、管理人の知識がしだいに息切れし始めたのも理由のひとつ。 しかし、『48時間でSchemeを書こう』の後半は、Parsecとは離れた、Haskell プログラミングのコツが満載されている。そのテクニックを細切れに取り出して、プログ記事にできればいいなと思っている。いずれにせよ、宝物を手に入れた気分だ。
by tnomura9
| 2011-12-13 07:27
| Haskell
|
Comments(0)
|
カテゴリ
新型コロナウイルス 主インデックス Haskell 記事リスト 圏論記事リスト 考えるということのリスト 考えるということ ラッセルのパラドックス Haskell Prelude Ocaml ボーカロイド 圏論 jQuery デモ HTML Python ツールボックス XAMPP Ruby ubuntu WordPress 脳の話 話のネタ リンク 幸福論 キリスト教 心の話 メモ 電子カルテ Dojo JavaScript C# NetWalker ed と sed HTML Raspberry Pi C 言語 命題論理 以前の記事
最新のトラックバック
最新のコメント
ファン
記事ランキング
ブログジャンル
画像一覧
|
ファン申請 |
||