「Write Yourself a Scheme in 48 Hours」 を写経してみる(5) : エラーチェックと例外

-- readExpr は (Right) パースに成功するとパースした値を返す。
-- 失敗のときは (Parser err) の例外を投げる。
readExpr :: String -> ThrowsError LispVal
readExpr input = case parse parseExpr "lisp" input of
    Left  err -> throwError $ Parser err
    Right val -> return val
data LispError = NumArgs Integer [LispVal]
               | TypeMismatch String LispVal   -- 期待した型と一致しない
               | Parser ParseError             -- パースエラー
               | BadSpecialForm String LispVal -- eval パーターンにマッチしない
               | NotFunction String String     -- func が見つからない
               | UnboundVar String String
               | Default String

showError :: LispError -> String
showError (UnboundVar message varname)  = message ++ ": " ++ varname
showError (BadSpecialForm message form) = message ++ ": " ++ show form
showError (NotFunction message func)    = message ++ ": " ++ show func
showError (NumArgs expected found)      = "Expected " ++ show expected 
                                          ++ " args; found values " ++ unwordsList found
showError (TypeMismatch expected found) = "Invalid type: expected " ++ expected
                                          ++ ", found " ++ show found
showError (Parser parseErr)             = "Parse error at " ++ show parseErr

instance Show LispError where show = showError

instance Error LispError where
     noMsg = Default "An error has occurred"
     strMsg = Default

type ThrowsError = Either LispError

trapError :: (MonadError e m, Show e) => m String -> m String
trapError action = catchError action (return . show)
--  :t catchError
-- catchError :: MonadError e m => 
--               m a ->                 -- エラー
--              (e -> m a) ->           -- エラーを返値に変換する関数
--               m a

extractValue :: ThrowsError a -> a
extractValue (Right val) = val

main を部分的に実行してその様子を見てみます。

main :: IO ()
main = do
    args <- getArgs
    evaled <- return $ liftM show $ readExpr (args !! 0) >>= eval
    putStrLn $ extractValue $ trapError evaled

パースエラーと例外
S式の文字列をパースして内部データに変換する readExpr の返す型は ThrowsError LispVal です。

> ghci listing5.hs
> :t readExpr "(+ 1 2)" 
readExpr "(+ 1 2)" :: ThrowsError LispVal

> readExpr "(+ 1 \"two\")" 
Right (+ 1 "two")
> readExpr "(add 1 \"two\")" 
Right (add 1 "two")
-- パースに失敗すると例外を投げます。
> readExpr "((add 1 \"two\")" 
Left Parse error at "lisp" (line 1, column 15):
unexpected end of input
expecting space or ")"

評価エラーと例外
eval は正常に評価出来たときは LispVal を返し、評価に失敗すると LispError を返します。

> :t readExpr "(+ 1 2)" >>= eval
-- > readExpr "(+ 1 2)" >>= eval :: Either LispError LispVal

--  eval のパーターンにマッチしないとき
--  BadSpecialForm 型のエラーを返す。
>  readExpr "abc" >>= eval  
-- > Left Unrecognized special form: abc

-- apply 関数の中で primitives の中から lookup にて func を探したけれども見つからなかった。
-- maybe が NotFunction 型のエラーを返す。
> readExpr "(add 1 2)" >>= eval
-- > Left Unrecognized primitive function args: "add"

-- 引数となるリストを展開しているとき、期待した型以外のデータにあたった。
> readExpr "(+ 1 \"two\")" >>= eval
-- > Left Invalid type: expected number, found "two"

show を適用することにより文字列に変換。

> readExpr "(+ 1 2)" >>= eval                -- > Right 3
> liftM show $ readExpr "(+ 1 2)" >>= eval   -- > Right "3"

trapError により例外を捕捉し、エラーメッセージを文字列に。

> return $ liftM show $ readExpr "(+ 1 \"two\")" >>= eval
-- > Left Invalid type: expected number, found "two"

> trapError =<< (return $ liftM show $ readExpr "(+ 1 \"two\")" >>= eval ) 
-- > Right "Invalid type: expected number, found \"two\""
> trapError =<< (return $ liftM show $ readExpr "(+ 1 2)" >>= eval ) 
-- > Right "3"

extractValue で Right を外している。

> extractValue $ trapError =<< (return $ liftM show $ readExpr "(+ 1 2)" >>= eval ) 
-- > "3"
> extractValue $ trapError =<< (return $ liftM show $ readExpr "(+ 1  \"two\")" >>= eval ) 
-- > "Invalid type: expected number, found \"two\""