棚上げになっていたCatyスクリプトの例外処理ですが、「これでいいかな」という案を考えました。型推論(静的型解析)に目星が付いたので、それをベースに他のことを考えることができるようになった、という事情です。以下に一通り説明し、最後に補足説明を付けます。
内容:
- Catyスクリプトで扱える例外
- catch式
- catch-handle式
- 宣言されていない例外
- いろいろと補足
Catyスクリプトで扱える例外
ネイティブコマンドは、実装言語の例外機構により例外を発生させてもかまいません。というか、それを避けることは事実上不可能です。ただ、コマンドが発生させたどんな例外でもCatyスクリプトで扱えるかというと、それは無理なので、一定の基準を満たす例外に限ってCatyスクリプト内で捕捉とハンドルをします。
まず、投げられる例外データの型は、前もって型定義しておく必要があります。
@exception
type FileNotFound = @FileNotFound object {
"filePath" : string(remark="ファイルパス"),
"stackTrace" : opaque // opaqueはCatyでは関知しないデータ型
};
@exceptionアノテーションは、その型を例外として使えることを表します(例外以外の目的で使ってもかまいません)。例外型は、必ずタグ付きでなくてはならず、そのタグが例外の種別を表します。タグ名の名前空間はフラットなので、階層的種別は提供しません。偶発的に同じタグを使ってしまった場合でも、同種の例外とみなされます。
例外の発生可能性はコマンド宣言に書きます。throwsに書けるのは、前もって例外型として宣言された型だけです。
command read-file [string(remark="ファイルパス")]
:: void -> _FileContent throws FileNotFound
refers python:mafslib.ReadFile;
catch式
catch {式}
とすると、式のなかで発生した例外(ただし、Catyの例外)は捕捉され、捕まえた例外データがそのまま値として出力されます。catchブロック内部で、例外発生時点より後に実行予定だった部分は実行されません。
このcatch式は、古典的な(今ではほとんどすたれた)throw-catchセマンティクス*1を持ちます。例外という異常事態であることを無視して、単なる値に変換してしまうので、あまりよろしくないのですが、対話的に使うときなどはお手軽で便利です。
catch-handle式
catch {式} handle {ハンドラー}
という構文を使うと、捕捉した例外をどうすべきかをハンドラー部に書けます。handle {ハンドラー} 部分の構文は、when式と同じです。ただし、例外が起きなかった場合を示すために、特殊な記号'-'をタグ代わりに使います。
catch {read-file /tmp/test.txt}
handle {
FileNotFound => print /tmp/not-found-error.html | break,
- ==> pass
} |
print /tmp/show-text.html
「- ==> pass」はよく使われる上に、「- => pass」と間違って書くと期待してない結果となる(タグを剥ぎ取ってしまう)場合があるので、省略することができ、省略すると「- ==> pass」とみなされます。handle節内に列挙してない例外は捕捉されず、その外側のcatch-handle式、最後はシェルによりハンドルされます。
handle節がないcatch式の場合、次のようなhandle節が処理系により補われると考えます。
catch {read-file /tmp/test.txt}
handle {
FileNotFound ==> pass,
- ==> pass
}
正常値も異常値(例外データ)もそのまま出力に流されるので区別が付かなくなるわけです。
宣言されていない例外
command宣言文にthrows節*2がなくても、それは例外が発生しないことを意味しません。また、throws節があっても、それ以外の例外が起きない保証はありません。ネイティブコマンドが、宣言されてない例外を発生させた場合は、そこで全体の評価は中止され失敗します。throws節で宣言されてない例外を、Catyスクリプト内で捕捉することはできず、評価を続けることもできません。インタプリタ自身が発生させるランタイムエラー(型のミスマッチなど)をスクリプトで捉えることもできません。
Webから起動されたCatyスクリプトの評価が失敗すると、ステータスコード500番が返され、エラーメッセージやスタックトレースがログに書き出されます。
いろいろと補足
型の名前は、パッケージ名とモジュール名で修飾できます。つまり、例外型は階層化できるのです。それなのになぜ、フラットなタグ名を分類に使うかというと、型名はインスタンスには記録されてないからです。捕まえた例外データ(JSONです!)をいくら眺めても型はわからないのです。そこで、when式と同じくタグ名による分岐になるわけ。型定義のとき、タグ名の衝突があれば警告することはできます。
記号「=>」の意味が以前とは変わっています。「=>」を通るときにデータのタグが剥ぎ取られます。「==>」を使うと、タグ付きのままで引き続く処理に流されます。
サンプルに出した次のコードはちょっと見にくいです。
catch {read-file /tmp/test.txt}
handle {
FileNotFound => print /tmp/not-found-error.html | break,
- ==> pass
} |
print /tmp/show-text.html
passを省略して:
catch {read-file /tmp/test.txt}
handle {
FileNotFound => print /tmp/not-found-error.html | break,
} | print /tmp/show-text.html
あるいは:
catch {read-file /tmp/test.txt}
handle {
FileNotFound => print /tmp/not-found-error.html,
- ==> print /tmp/show-text.html
}
Catyスクリプトは長くなると可読性が悲惨なことになるので、1行でも短く書いたほうがいいと思います。えっ? 可読性が悪くていいんかって。いいんです。「長いCatyスクリプトは書くな」ってのがCatyのメッセージですから。
割と無難にまとまったので、普通の(ワイルドでない)Catyスクリプトに入れてもいいかな、という気もしますが、無難てことは中途半端ってことでもあります。
- 例外という概念がそもそも難しいって事実は変わらないので、学習負担になる概念を入れるべきかどうかは、やはり問題。
- テストスクリプトを書くには例外捕捉が必須だが、その目的では捕捉能力が弱い気がする。
ウーム。
[追記]タイトルが「ほぼ決まりかな」なのに、最後は「ウーム」。あんまり決まりじゃないかもな。[/追記]