「48時間でSchemeを書こう/エラー処理と例外」の版間の差分

削除された内容 追加された内容
部分的に訳出
訳出
1 行
現在のところ、コード中の様々な場所でエラーを無視するか、暗黙の内に<code>#f</code>や<code>0</code>などの「デフォルト値」をassignし与えています。いくつかの言語 - PerlやPHP等 - はこの方針で問題ないようです。しかしながら、それは大抵エラーが表面上は問題ないようにプログラム中を推移し、のちに大きな問題となって顕れるという、プログラマにとってデバッグしにくいものとなることを意味します。エラーが起こったその時にそれを報告し、直ちに実行を中止できればいいですね。
 
そのためには、第一に、Haskellの組み込みのエラー関数を使えるよう<code>Control.Monad.Error</code>をインポートする必要があります。
67 行
</syntaxhighlight>
 
<code>trapError</code>の結果は、常に非エラー値(<code>Right</code>)を持つ<code>Either</code>アクションです。他の関数から値を利用できるように、<code>Either</code>モナドから値を取り出す方法も用意しなくてはなりません
The result of calling trapError is another Either action which will always have valid (Right) data. We still need to extract that data from the Either monad so it can passed around to other functions:
 
<syntaxhighlight lang="haskell">
75 ⟶ 74行目:
</syntaxhighlight>
 
<code>extractValue</code>は<code>Left</code>コンストラクタに関して意図的に未定義にしてあります。Leftの時はHaskell側にエラーがあることを表すからです。<code>extractValue</code>を<code>catchError</code>の後にのみ使うことにしているので、変な値を残りのプログラム中に入れ込むよりはさっさと失敗する方がよいです。
We purposely leave extractValue undefined for a Left constructor, because that represents a programmer error. We intend to use extractValue only after a catchError, so it's better to fail fast than to inject bad values into the rest of the program.
 
これで基本的なインフラが整ったので、今度はエラー処理関数を使う段です。私たちのパーサがエラー時に単に<code>"No match"</code>という文字列を返していたことを覚えていますか?それを<code>ParseError</code>にラップして投げるようにさせましょう。
Now that we have all the basic infrastructure, it's time to start using our error-handling functions. Remember how our parser had previously just return a string saying "No match" on an error? Let's change it so that it wraps and throws the original ParseError:
 
<syntaxhighlight lang="haskell">
readExpr :: String -&gt; <span class="changed_code">ThrowsError</span> LispVal
readExpr :: String -> ThrowsError LispVal
readExpr input = case parse parseExpr "lisp" input of
readExpr input = case parse parseExpr "lisp" input of
Left err -&gt; <span class="changed_code">throwError $ Parser err</span>
Left err -> throwError $ Parser err
Right val -&gt; <span class="changed_code">return</span> val
Right val -> return val
</syntaxhighlight>
 
Here, we first wrap the original ここでは、まず<code>ParseError with the </code>を<code>LispError constructor </code>のコンストラクタである<code>Parser, and then use the built-in function </code>でラップし、組み込み関数[http://www.haskell.org/ghc/docs/latest/html/libraries/mtl/Control-Monad-Error.html#v%3AthrowError throwError] to return that in our を使って<code>ThrowsError monad. Since </code>モナドにして返します。<code>readExpr now returns a monadic value, we also need to wrap the other case in a </code>がモナド値を返すようになったので、もう一方の場合も<code>return</code>でラップしなければなりません(<code>return function.val</code>)。
 
次に、<code>eval</code>の型をモナドを返すように変え、戻り値をそれに合うように変えて、知らないパターンに出会ったときにエラーを投げる節を加えます。
Next, we change the type signature of eval to return a monadic value, adjust the return values accordingly, and add a clause to throw an error if we encounter a pattern that we don't recognize:
 
<syntaxhighlight lang="haskell">
eval :: LispVal -&gt; <span class="changed_code">ThrowsError</span> LispVal
eval :: LispVal -> ThrowsError LispVal
eval val@(String _) = <span class="changed_code">return</span> val
eval val@(NumberString _) = <span class="changed_code">return</span> val
eval val@(BoolNumber _) = <span class="changed_code">return</span> val
eval val@(ListBool [Atom "quote", val]_) = <span class="changed_code">return</span> val
eval (List ([Atom func : args)) = <span class="changed_codequote">mapM, eval argsval]) &gt;&gt;= applyreturn funcval
eval (List (Atom func : args)) = mapM eval args >>= apply func
eval badForm = throwError $ BadSpecialForm "Unrecognized special form" badForm</span>
eval badForm = throwError $ BadSpecialForm "Unrecognized special form" badForm
</syntaxhighlight>
 
関数適用の節が<code>eval</code>(モナドを返す)を再帰的に呼ぶので、その節を変える必要があります。まず、<code>map</code>を[http://www.haskell.org/onlinereport/standard-prelude.html#$vmapM mapM]、モナドを扱う関数を値のリストにmapし、bindで結果の値を配列し、モナド中の値の結果をリストで返す関数に変えます。<code>Error</code>モナドでは、この配列は全ての計算を順番に行いますが、その内のどれか一つでも失敗すればエラーを返し、<code>Right [results]</code>を成功時に、<code>Left error</code>を失敗時に返します。そして、モナドのbindを使って部分適用された<code>apply func</code>にその結果を渡し、ここでも前の操作が失敗であったならばエラーを返します。
Since the function application clause calls eval (which now returns a monadic value) recursively, we need to change that clause. First, we had to change map to [http://www.haskell.org/onlinereport/standard-prelude.html#$vmapM mapM], which maps a monadic function over a list of values, sequences the resulting actions together with bind, and then returns a list of the inner results. Inside the Error monad, this sequencing performs all computations sequentially but throws an error value if any one of them fails - giving you <code>Right [results]</code> on success, or <code>Left error</code> on failure. Then, we used the monadic "bind" operation to pass the result into the partially applied "apply func", again returning an error if either operation failed.
 
次に、与えられた関数を認識しなければエラーを投げるように<code>apply</code>自身を変えます。
Next, we change apply itself so that it throws an error if it doesn't recognize the function:
 
<syntaxhighlight lang="haskell">
apply :: String -&gt; [LispVal] -&gt; <span class="changed_code">ThrowsError</span> LispVal
apply :: String -> [LispVal] -> ThrowsError LispVal
apply func args = maybe <span class="changed_code">(throwError $ NotFunction "Unrecognized primitive function args" func)</span>
apply func args = maybe (throwError $ NotFunction "Unrecognized primitive function args" func)
($ args)
($ args)
(lookup func primitives)
</syntaxhighlight>
 
私たちは<code>return</code>を関数適用<code>($ args)</code>に加え''ません''でした。代わりに、プリミティブの型を変え、<code>lookup</code>から返された関数自身<code>ThrowsError</code>アクションを返すようにします。
We ''didn't'' add a return statement to the function application ($ args). We're about to change the type of our primitives, so that the function returned from the lookup itself returns a ThrowsError action:
 
<syntaxhighlight lang="haskell">
primitives :: [(String, [LispVal] -&gt; <span class="changed_code">ThrowsError</span> LispVal)]
primitives :: [(String, [LispVal] -> ThrowsError LispVal)]
</syntaxhighlight>
 
そして、もちろん、それらプリミティブを実装する<code>numericBinop</code>関数を、一つしか引数が与えられなければエラーを投げるように変えなければいけません。
And, of course, we need to change the numericBinop function that implements these primitives so it throws an error if there's only one argument:
<syntaxhighlight lang="haskell">
numericBinop :: (Integer -&gt; Integer -&gt; Integer) -&gt; [LispVal] -&gt; <span class="changed_code">ThrowsError</span> LispVal
numericBinop :: (Integer -> Integer -> Integer) -> [LispVal] -> ThrowsError LispVal
<span class="changed_code">numericBinop op singleVal@[_] = throwError $ NumArgs 2 singleVal</span>
numericBinop op singleVal@[_] = throwError $ NumArgs 2 singleVal
numericBinop op params = <span class="changed_code">mapM unpackNum params &gt;&gt;= return . Number . foldl1 op</span>
numericBinop op params = mapM unpackNum params >>= return . Number . foldl1 op
</syntaxhighlight>
 
一引数のみの場合を捉えるには、@パターンを使います。エラー報告のために実際に渡された引数自体を使いたいからです。ここで、私たちは正確に一要素だけのリストを求めていて、かつその要素が何であるかは問題ではない。また、私たちは<code>unpackNum</code>の結果を並べるのに<code>mapM</code>を使わねばならず、それは<code>unpackNum</code>の呼び出しそれぞれが<code>TypeMismatch</code>で失敗するかもしれないからです。
We use an at-pattern to capture the single-value case because we want to include the actual value passed in for error-reporting purposes. Here, we're looking for a list of exactly one element, and we don't care what that element is. We also need to use mapM to sequence the results of unpackNum, because each individual call to unpackNum may fail with a TypeMismatch:
 
<syntaxhighlight lang="haskell">
unpackNum :: LispVal -&gt; <span class="changed_code">ThrowsError</span> Integer
unpackNum :: LispVal -> ThrowsError Integer
unpackNum (Number n) = <span class="changed_code">return</span> n
unpackNum (StringNumber n) = let parsed = readsreturn n in
unpackNum (String n) = let parsed = reads n in
if null parsed
if null parsed then <span class="changed_code">throwError $ TypeMismatch "number" $ String n</span>
then elsethrowError <span$ TypeMismatch class="changed_codenumber">return $ </span>fst $ parsed !!String 0n
else return $ fst $ parsed !! 0
unpackNum (List [n]) = unpackNum n
unpackNum (List [n]) = unpackNum n
<span class="changed_code">unpackNum notNum = throwError $ TypeMismatch "number" notNum</span>
unpackNum notNum = throwError $ TypeMismatch "number" notNum
</syntaxhighlight>
 
最後に、この一連の巨大なエラーモナド群を使うために<code>main</code>関数を変える必要があります。これは<code>IO</code>と<code>Error</code>という''二つの''モナドを扱わなければいけなくなるので、ちょっと複雑になるかもしれません。なので、またdo記法を使うことにします。というのも一つのモナドが他のモナドに入れ子になっているときにpoint-free styleを使うのはほぼ不可能だからです。
Finally, we need to change our main function to use this whole big error monad. This can get a little complicated, because now we're dealing with ''two'' monads (Error and IO). As a result, we go back to do-notation, because it's nearly impossible to use point-free style when the result of one monad is nested inside another:
 
<syntaxhighlight lang="haskell">
main :: IO ()<span class="changed_code">
main =:: doIO ()
main = do
args &lt;- getArgs
args <- getArgs
evaled &lt;- return $ liftM show $ readExpr (args !! 0) &gt;&gt;= eval
evaled <- return $ liftM show $ readExpr (args !! 0) >>= eval
putStrLn $ extractValue $ trapError evaled
putStrLn $ extractValue $ trapError evaled
</span>
</syntaxhighlight>
 
この新たな関数がやっていることは以下の通りです。
Here's what this new function is doing:
 
# <code>args</code>はコマンドライン引数のリスト
# args is the list of command-line arguments
# <code>evaled is the result of:</code>は以下の結果
## taking first argument 最初の引数を取って(<span class="inline_code"code>args !! 0</spancode>)
## parsing it パースして(<span class="inline_code"code>readExpr</spancode>)
## <code>eval</code>に渡して(<code>>>= eval</code> - bind演算子は関数適用より高い優先順位を持つ)
## passing it to eval (<span class="inline_code">&gt;&gt;= eval</span><nowiki>; the bind operation has higher precedence than function application)</nowiki>
## <code>Error</code>モナドの中の値に対して<code>show</code>を呼ぶ。アクション全体が<code>IO (Either LispError String)</code>型を持つので、<code>evaled</code>が<code>Either LispError String</code>型を持つことに注意してください。<code>trapError</code>関数がエラーを<code>String</code>にのみ変換でき、その型は普通の値の型に適合しなければならないので、そうでなくてはなりません。
## calling show on it within the Error monad. Note also that the whole action has type IO (Either LispError String), giving evaled type Either LispError String. It has to be, because our trapError function can only convert errors to Strings, and that type must match the type of normal values
# <code>caught is the result of</code>は以下の結果
## <code>trapError</code>を<code>evaled</code>に対して呼び、エラーをその文字列表現に変える
## calling trapError on evaled, converting errors to their string representation.
## calling <code>extractValue to get a </code>を呼び<code>String out of this </code>を<code>Either LispError String action</code>アクションから取り出す
## <code>putStrLn</code>で結果を表示
## printing the results through putStrLn
 
新しいコードをコンパイル・実行して、いくつかエラーを投げさせてみてください。
Compile and run the new code, and try throwing it a couple errors:
 
<syntaxhighlight lang="text">
$ ghc -package parsec -o errorcheck [../code/listing5.hs listing5.hs]
% ghc -package parsec -o errorcheck [../code/listing5.hs listing5.hs]
$ ./errorcheck "(+ 2 \"two\")"
% ./errorcheck "(+ 2 \"two\")"
Invalid type: expected number, found "two"
Invalid type: expected number, found "two"
$ ./errorcheck "(+ 2)"
% ./errorcheck "(+ 2)"
Expected 2 args; found values 2
Expected 2 args; found values 2
$ ./errorcheck "(what? 2)"
% ./errorcheck "(what? 2)"
Unrecognized primitive function args: "what?"
</syntaxhighlight>
 
Some readers have reported that you need to add a --make flag to build this example, and presumably all further listings. This tells GHC to build a complete executable, searching out all dependencies listed in the import statements. The command above works on my system, but if it fails on yours, give --make a try.
このコードをビルドするには<code>--make</code>フラグと、予想されるように、これまでのlisting全てを加える必要があると何人かの読者から報告を受けました。これはGHCに<code>import</code>文に記された依存関係全てを探し出して完全な実行ファイルをビルドするように指示します。上のコマンドは私のシステムでは上手くいきますが、あなたのところで駄目だった場合、<code>--make</code>を試してみてください。