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

自前のエラーモナドの作り方。

Control.Monad.Error モジュールを使うと、自前のエラーモナドで例外処理ができるようになるらしい。説明を読んでもよくわからなかったので、all about monads の例をコピーして実験してみた。

まず、次の5行だけを errormonad.hs に作成して、ghci で試した。

import Control.Monad.Error
import Data.Char

data ParseError = Err {location::Int, reason::String}
  deriving Show

実行例:
*Main> Err 0 "error"
Err {location = 0, reason = "error"}

つぎに次の2行を付け加えて ParseError を Error クラスのインスタンスにした。noMsg と strMsg を定義するのが大切らしい。

instance Error ParseError where
  noMsg = Err 0 "Parse Error"
  strMsg s = Err 0 s

試してみた:
*Main> noMsg::ParseError
Err {location = 0, reason = "Parse Error"}

その次にやったのは、ParseMonad というタイプを定義して ParseError を Either 型でラッピングした。そうすると、関数の型宣言のとき戻り値を ParseMonad a というタイプにしておけば、正常実行のときは Right a が、エラーが起きると Left ParseError が戻される。

type ParseMonad = Either ParseError

parseHexDigit :: Char -> Int -> ParseMonad Integer
parseHexDigit c idx =
  if isHexDigit c
    then
      return (toInteger (digitToInt c))
    else
      throwError (Err idx ("invalid character '" ++ [c] ++ "'"))

実験した。
*Main> parseHexDigit 'a' 0
Right 10
*Main> parseHexDigit 'x' 0
Left (Err {location = 0, reason = "invalid character 'x'"})

うまく例外が発生した。自前のデータ型のエラー値をもっていると、ほかのエラー型と別に管理ができるので便利だ。instance 宣言で Error クラスのインスタンスにして noMsg と strMsg に登録して、ParseMonad で ParseError を Either 型のモナドでラッピングしてやれば、Right と Left に分けて出力してくれる。意外と簡単で便利な機能だった。例外の発生は throwError を ParseError に関数適用させればいい。しくみはよくわからないがなんとなく使い方がつかめた。

残りの部分を追加した errormonad.hs の全体はつぎのようになる。

import Control.Monad.Error
import Data.Char

data ParseError = Err {location::Int, reason::String}
  deriving Show

instance Error ParseError where
  noMsg = Err 0 "Parse Error"
  strMsg s = Err 0 s

type ParseMonad = Either ParseError

parseHexDigit :: Char -> Int -> ParseMonad Integer
parseHexDigit c idx =
  if isHexDigit c
    then
      return (toInteger (digitToInt c))
    else
      throwError (Err idx ("invalid character '" ++ [c] ++ "'"))

parseHex :: String -> ParseMonad Integer
parseHex s = parseHex' s 0 1
  where
    parseHex' [] val _ = return val
    parseHex' (c:cs) val idx = do
      d <- parseHexDigit c idx
      parseHex' cs ((val * 16) + d) (idx + 1)

toString :: Integer -> ParseMonad String
toString n = return $ show n

convert :: String -> String
convert s =
    let (Right str) = do {n <- parseHex s; toString n} `catchError` printError
    in str
  where
    printError e = return $ "At index " ++ (show (location e)) ++ ":" ++ (reason e)

throwError で発生させた例外は catchError で捕捉することができる。catchError の使い方は Prelude の catch とほぼ同じ。

実行例:
*Main> convert "3e"
"62"
*Main> convert "3x"
"At index 2:invalid character 'x'"

複雑にみえたエラー・モナドの使い方は、一つずつの部品を試していったらそれほど難解でもなかった。Haskell のいいところは、プログラムが関数なので、部品のテストが容易にできるということだ。これで、ふたたび『48時間で作るScheme』に挑戦する準備ができた。
by tnomura9 | 2011-12-12 06:08 | Haskell | Comments(0)
<< Either モナド? Haskell の例外処理 >>