パスワードを忘れた? アカウント作成
13265992 journal
日記

m_nukazawaの日記: Edge of exceptional exception(草稿) 1

日記 by m_nukazawa

きっかけは、会社で昼休み中に見つけたTogetterのこのまとめ。曰く、bashシェルスクリプトを書く際、何も考えずに`set -e`を頼るのはよくないのではないか、という。

挙げられている理由は大きく2つ。
ひとつ目は、エラーこそが欲しい結果である場合。あるいはvariant型の偉大な力によって、出力がエラーに変換されてしまう等の場合があること。途中解で意図的にエラーを使うことが難しく、シェルスクリプトに書ける処理が制約を受けてしまう。
もうひとつは、`set -e`には思いもよらない挙動が多く、シェルスクリプト関数の中などでプログラマの思う通りにエラーを捕捉してくれない場合があるのだという。もしその通りであるのなら、確かにエラー処理として頼るには心もとないかもしれない。
まとめ中では、`set -e`を使ってまとめてエラーを処理しようとせず、コマンドまたはパイプ終端ごとにひとつずつ、確実にエラー処理を書くことを推奨していた。

一通りTogetterを読み終わり、得た情報を元に、いま使っているシェルスクリプトを精査すべきか少し考えてみた。
まず、仕事のプロジェクト。こちらは問題無さそうだった。
次に、趣味のプロジェクト群。vecterionでは、テストの一部やビルドシステムの外苑にbashシェルスクリプトが組み付けられている。またRuneAMN等のFont系のプロジェクトではbashシェルスクリプトがビルドシステムのグルーとして重要な役割を果たしている。vecterionのテストスクリプトはTDDよろしくエラーが出るのを確認しながら書いた。Fontのビルドシステムは、最終的にフォントが出力されれば良いのであって、多くの場合、結果を見れば問題があることは一目瞭然にわかる。
`set -e`は強力なエラー処理方法だ。エラー処理がスクリプト先頭のtrapにまとまるので、コードが読みやすくなる。コマンド行毎に終了コードのチェックを入れたら、シェルスクリプトの可読性が下がってしまう。
結論として、わたしは`set -e`を使い続けることにした。確かに危険はあるのだろうが、今のところ問題は起こっていない。たぶんコントロール可能な範囲だろう。bashシェルスクリプトで関数なんて書かないし(そうでもないかな...ちょっと心配)。
プログラマがエッジケースの存在を知ってさえいれば、問題にハマって時間をいたずらに費やしてしまう心配はない。次からは気をつけよう、というか、今度、予想と違うことが起こった時には`set -e`を疑って、Web上の情報を探しなおせば良い。具体的な問題と回避方法は、実例から失敗を学んだほうが覚えも良いだろう。
こうして『`set -e`不要論』は昼休みのネットサーフィンの収穫として、ちょっとしたAssertionとまあまあの満足感を与えてくれた。その時はそれで終わった。

帰宅後、ターミナルを叩いて謹製のMakefileが自作フォントを吐き出す様子を眺めていた。Makefileがotfフォントファイルを生成する様は若干倒錯的だ。わたしは小さな幸福感に浸っていた。
そのとき、ふと昼に見た『`set -e`費用論』を思い出した。例外的な状況では、`set -e`が意図通りに働かない場合があること。いや、例外的というほど特別な状況ではなくて、普通のシェルスクリプトを書いていて起こりうるシチュエーションなのではないか。学習効率は置いておいて、いちど、ケース集に目を通すべきだろうか。
そのとき脳内でWarningが上がった。エラー発生箇所、つまり直前の思考の文字出力へ意識がジャンプする。

例外的?
`set -e`は例外に似ている。

exceptionのイメージが脳裏に去来する。言語構文に定義された例外。C++は気が乗らない。エラー処理のベストプラクティス。握り潰し。OSによるエラーダイアログ。
`set -e`も例外も、戻り値のチェックからコードとプログラマを開放してくれる。エラー処理を正常系と書き分けることで、正常系の流れが読み取りやすいコードになる。可読性が高くなればコードの保守性も高まる。コード内のすべてのエラーを自動的に捕捉することで、プログラマのミスによるエラー処理漏れを起こさなくする。
完璧なコードを書けない私たち。不完全な人間のためのすばらしいソリューション。ただしそれは、人間に完璧なエラー処理のコーディングを要求する。
十分に理解のあるプログラマが扱うならば、例外は安全で有用なものであり、`set -e`は理想通りにすべてのエラーを捕捉してくれる。例外をハンドリングしきれずに握り潰してしまうプログラマと、`set -e`のエッジケースをすべて把握しておくことのできないプログラマの側に問題がある。だが、その考えは問題を言語から追い出しただけで、問題を解決したわけではない。でもエラー処理を書かなくて良いのは楽で良いよね。
例外を諦め、簡潔だった正常系が長大な異常系への対処コードに無残に切り刻まれて埋もれることを受け入れて、関数を呼び出す度に戻り値をチェックするコードを丁寧に書くべきだ。
『今度は例外なしでやりましょうよ』。自分が先週、言ったばかりのセリフを思い出す。

自分はどちらを支持しているのだ?

例外をことさらに嫌悪しておきながら、他方で似た機能である`set -e`を擁護している自分に気づく。少なからず心がざわついた。

両者には違いもある。例外はtry{}catch{}構文で、エラー処理は後に来る。例外は回復できる。通常処理に復帰するためにあるとも言える。
`set -e`は先に来る。わたしはtrapを一緒に使ってエラーの発生行を示し、そして通常処理に戻らず終了する。事実上のクラッシュ。
```
    3 set -ue¬
    4 ¬
    5 trap 'echo "$0(${LINENO}) ${BASH_COMMAND}"' ERR¬
```
『エラーが起こったら、直ちにけたたましく警告を出し、そして速やかにクラッシュするべし』というのはUnix思想のプラクティスだったか。
エラーに陥ると、通常処理への復帰は難しい。復帰したつもりになって、床に落ちたアイスクリームを塗り広げる掃除ロボットのようなことになっては目も当てられない。
`set -e`はクラッシュを起こすための仕組みであり、例外はエラーから回復するための構文だ。前者はシンプルで、後者は複雑になりやすい。複雑な例外処理はエラーにバグの上塗りをするリスクが高く、見た目にも汚いコードになる傾向が強い。小学校で雑巾が嫌われるのを見るようだ。床拭きにはクイックルワイパーを使って、掃除が終わったらゴミ箱に捨ててしまえばいい。
あのJoel社長がむかし書いていたような気がする。『やっつけのスクリプトを例外を使って書くのはすばらしい。そしてそのコードが翌朝クラッシュしていたら、その時はやれやれと言いながら人間が対処する。それで何の問題もない』

それはその日、同僚と話していた話を思い出させる。サーバのサンプルコードには、実用性に一歩足りないところがあるという話だ。サーバアプリケーションが立ち上がり、クライアントとソケットを生成する。クライアントが処理を終え、サーバとのソケットを切断し、サーバはクライアントへのサービスで使用したすべての資源をクローズする。そしてサンプルコードは終了する。
そして、現実のサーバプログラムはサンプルコードのようには終わらない。人生は続いていく。サーバは次のクライアントを、その次のクライアントを、次の次のクライアントを待ち受けるために、自分を最初の状態にResetできなければならない。すべての資源を綺麗にクローズして初期状態に戻り、無限にループしていなければならない。だが正しいクローズは正常系でも難しい。ファイルディスクリプタのクローズ、シェアードメモリの参照カウントのデクリメント、読みかけのシリアルの破棄、fflash(3)、mallocしたメモリのfree、立てたままのpthreadの停止と削除、閉じっぱなしのMutexの開放に、アプリケーション内部の状態遷移の初期化。サンプルコードは、それらをすべてほったらかしたままExitすることで、後始末をOSに移譲する。
サンプルコードとしては正しい。サーバとしての役割を終えるところまでを一旦説明する説明の順番も、わかりやすくてありがたい。だが実用コードへ向けてもう一歩、難しいところの解説が必要なはずなのに、サンプルコードは優しげな顔をしたまま、これがすべてで十分と言わんばかりに、一番難しくて大切なところを説明してくれないで終わっている。

正常終了して正常処理に復帰するのは、かように難しい。いわんや、異常状態から正常処理へ復帰することの困難さよ。

思えば例外は、C++とJavaの目玉機能だった。エラー処理を簡略化する、銀の弾丸。少なくとも、わたしが例外を知ったのはC++には例外が有るという紹介からだ。
Javaは銀行のバッチシステムに採用された。それは、決してクラッシュしてはいけないシステムだった。大量の量産型Javaプログラマは例外を憎むことを知らなかったが、産業プログラミング業界にとっては新しいアイデアだった例外を彼らに教えるときに、例外の正しいハンドリング方法を教え損ねた。結果、無垢な量産型Javaプログラマたちは、あらゆるJavaの例外を単に『握り潰した』。
結果としてJavaの例外は、`set -e`したやっつけのbashスクリプトに劣る安全性を、銀行のバッチシステムにもたらした。少しは出来るシステムエンジニアが、再現性の低いデータ破壊を起こすJavaアプリケーションをデバッグしていて、まったく関係ないはずの箇所の関数呼び出しの奥底で、Javaプログラマがゼロ除算例外を握り潰しているのを発見した時の感情は、察するに余り有る。
C++はシステムプログラミング言語だった。`set -e`と異なり、あらゆるエッジケースで動作する回復可能な例外を実現するために、C++の例外はシステムプログラミングにはとてもではないが採用できない重装備なギミックになった。
組み込みエンジニアはC++の例外と、ついでにC++に対して期待を寄せることをやめた。無言で静かに首を横に振り、穏やかなC言語の世界に戻っていった。

結果的に、例外はその真価を発揮できない用途で、使い方が周知される前に普及してしまった。神速の重装騎兵に馬を降りて城塞拠点を守らせるようなミスマッチが、例外というアイデアから力を奪った。関数の戻り値を処理することのできない無知なプログラマが書いたゴミコードの責任を、例外は押し付けられた。
この状況下で、プログラマが例外を愛することは難しい。業界が、例外への強く深刻な嫌悪を生み出した。

例外というアイデアが過去の汚名を払拭し、古くて新しいアイデアとして再び脚光を浴びる日がきっとくる。それは、私たちが思うほど遠い未来ではないのだろう。だが、その日は明日にはまだ来ない。

まだエラー処理とそうでない何かを混同している気がするけれど、まあ間違っていれば指摘があるだろうから、これくらいで。

結論。
`set -e`不要論から始まった一連の連想のおかげで、例外に対する不要な嫌悪感に気づき、それを払拭することができた。例外は実は良いものだった。ただし、ただちにクラッシュするために使い、ただちにクラッシュできる用途に用いるならば。
さもなければ、例外は福音よりも多くの災厄を産み出すだろうから。

====
今年も夏がやって来ます。そろそろproject daisy bellに新しいメニューを増やしたいという機運もあり、ポエム、はじめました。
皆様のお口に合いますよう。ご愛顧頂ければ幸いです。

この議論は賞味期限が切れたので、アーカイブ化されています。 新たにコメントを付けることはできません。
  • by Anonymous Coward on 2017年04月29日 17時13分 (#3202956)

    どちらがいいか、なんて話はさんざん既出だろうし、私はそこまで徹底したコードは書けないので、置いといて。
    シェルスクリプトに近いかもしれないPerlの話を。正確でない部分もあろうかと思います。

    Perl5は20年ほど使われている言語です。昔は例外ではなくエラーコードによるチェックがほとんどすべてでした。
    後から書かれたモジュールは例外を返すものも多いですが、言語に組み込みなものとかは返り値で判定するタイプ。

    # Perl5は言語のコアな部分に例外処理というものはなく、コードをevalしてエラーが出たか、で例外処理としています

    もちろん返り値チェックは面倒なので、いろいろ考えられたわけです。その1つがautodieモジュール。
    1行、呪文「use autodie;」を唱えるだけで、エラーコードが返ってきたところで例外が発生するようになります。
    たしか、返り値を捨てているところだけ反応する、という動きだったはず。

    適当に検索してみつけたページ。
    http://d.hatena.ne.jp/noissefnoc/20110901/ [hatena.ne.jp]

    # 現在(2017年)時点で最高にベストな解法なのかはわかりません。
    # Perl6はまた別の話でしょう。私には知識がなく。

typodupeerror

人生の大半の問題はスルー力で解決する -- スルー力研究専門家

読み込み中...