たいたい竹流さん @torgtaitai のガード節に関するツイートについて、
@torgtaitai 同意ですが、共通の後処理考えてこうしてます。 result = NG for(;;){ if(! 判定 ){ //異常系処理 break } //正常処理 result = OK break } //後処理 return result
— 風柳 (@furyutei) 2015, 2月 14
のようにリプライしたところ、
いや、それなら doMain { result = doSub //後処理 } doSub { if( ! 判定 ) { //異常 reutnr NG } //正常 return OK } でいいじゃないですか。
— たいたい竹流 (@torgtaitai) 2015, 2月 14
のようにコメント頂いた。
これに端を発した議論が有意義に思えたので、自分用の覚書として要点をまとめておく。
■ ガード節について
ガード節そのものについては、自分は最初から積極的に賛同している立場であるため、この記事では特記しない。
ガード節の有効性については、下記記事の引用部分に集約されているように思う。
これは、「ガード節による入れ子条件記述の置き換え」と呼ばれるもので、リファクタリング本では次のように説明されている。小野和俊のブログ:ガード節を用いた if-then-else 文の置き換え
「ガード節による入れ子条件記述の置き換え」のキーポイントを強調しておきましょう。if-then-else 構造が使われる時は、if 部にも else 部にも同じウエイトが置かれています。これは、プログラムの読み手に対して両方共等しく起こり得ること、等しく重要であることを伝えます。逆に、ガード節は『めったに起きないが、起きた時には、何もしないで出ていく』ことを伝えます。
リファクタリング - 第九章 条件記述の単純化 p.251
■ 自分の発言の趣旨
上記であげた自分のリプライについてだが、もともと、複数のreturnを設けたり、Wrapper関数化するのに消極的だったのは、主にデバッグ上の要求から。
- 複数 return があると、そのすべてにブレイクポイントやログ出力処理などを設ける必要が出てくるので、出口は一つにしたい
- Wrapper化により異常系/正常系処理と共通の後処理とを切り離してしまうと、後処理内で異常系/正常系のローカル変数を参照できない
- 後処理を共通化するためだけの単純なWrapperを設けるのは、余分にコールスタックが増えるということであり、デバッグ時にコールスタックを表示させた際、ソースコードにおいてネストが深くなるのと同等の見通しの悪さが生じる
- Wrapperのために関数名が増えるのは面倒(苦笑)(言うまでもなく、これは副次的な理由)
なお、「ルールだから」という理由で途中 return を設けないわけではなく、デバッグ上の理由で消極的なだけなので、「必ず共通の後処理が実行される」保証があれば、途中で return することについては特に忌避感はない。
■ try-catch-finally で万全
実は、Java 等の try-catch-finally 相当の構文が使用できる言語であれば、次のように記述することで満足できる。
try { result = ABNORMAL; // [I] 共通の前処理(スコープ内で使用するローカル変数設定等) if ( /* 異常系判定条件 */ ) { // ガード節 // [A] 異常系処理 return result; } // ※状況に応じて複数のガード節を記述 // [N] 正常系処理 result = NORMAL; return result; } catch (Throwable errorObject) { // [E] 想定外の異常系(例外)処理 throw errorObject; } finally { // [F] 共通の後処理 // ※デバッグ用に、この部分にローカル変数を参照するログ出力処理等を入れたり // 条件付きブレイクポイントを設定したりできる }
try-catch-finallyは、try節中等に return があっても必ず finally 節が実行されるため、ブレイクポイントやログ出力処理などもここにまとめることができ、Wrapperを用意する必要もなくなり、上記の要求は全て満たされる。
■ try-catch-finally の類が使えない場合の代替手段
問題は、try-catch-finally のような「途中の経過に関わりなく後処理が実行される」という機能を持たない言語の場合。
たいたい竹流さんが募集して下さった意見などを元に、いくつかの代替手段とその問題点等をあげる。
◇ 一度のみ実行するループを用いた方法
result = ABNORMAL // [I] 共通の前処理(スコープ内で使用するローカル変数設定等) <ループ> { if ( /* 異常系判定条件 */ ) { // [A] 異常系処理 break } // [N] 正常系処理 result = NORMAL break } // [F] 共通の後処理 return result
for(;;){…} や while(true){…}、do{…}while(false)を使う手法。
問題点
- 初見の人には意図が判り辛い(do{…}while(false)であれば多少まし、ただし、言語によっては定数falseを使うことによる警告も)*1
- 一度しか実行されないループ→ループとして機能していない→構造化構文の誤用
◇ goto を用いた方法
result = ABNORMAL // [I] 共通の前処理(スコープ内で使用するローカル変数設定等) if ( /* 異常系判定条件 */ ) { // [A] 異常系処理 goto finally } // [N] 正常系処理 result = NORMAL finally: // [F] 共通の後処理 return result
問題点
- 前方への移動等の乱用を誘発しないか懸念(適切な使い方しか成されない保証があるのであれば可)
◇ 関数内関数を用いた方法
result = ABNORMAL // [I] 共通の前処理(スコープ内で使用するローカル変数設定等) (<関数>(){ if ( /* 異常系判定条件 */ ) { // [A] 異常系処理 return; } // [N] 正常系処理 result = NORMAL return; })() // [F] 共通の後処理 return result
問題点
- 一見してあえて関数内関数を使う意図が判り辛い
- 本来スコープ内に閉じ込めるべきローカル変数を外出ししている(必要な変数をすべて引数として渡す、という方法もあるが、それならば最初から外部関数なりにするのと変わらず、内部関数にするメリットがない)
◇ classのデストラクタを利用した方法
<クラス定義> { result = ABNORMAL // [I] 共通の前処理(スコープ内で使用するローカル変数設定等) <メイン処理>(…) { if ( /* 異常系判定条件 */ ) { // [A] 異常系処理 return result } // [N] 正常系処理 result = NORMAL return result } <デストラクタ>(…) { // [F] 共通の後処理 } }
問題点
■ コーディングルールにおける「一律禁止」問題
上記の議論に付随して、「コーディングルールにおける、思考を停止した『一律禁止』は問題である」という話も出た。
知らず知らずのうちに思考停止に陥っていることが多いので、自戒を込めてまとめる。
ルールには、もちろんそれが制定された意味はある(場合がほとんどだ)けれども、盲目的に従うのではなく、歴史的な経緯を把握した上で、現状と照らし合わせてきちんと考えようということだと思う。
とはいえ、クライアントに要求されたルールに従わざるを得ない等、なかなか理想通りには行かないことも多いのだけれど……せめて吟味し批判する姿勢は保ちたいもの。
- そのルールは現状に即しているのか? (「速度・リソース的な問題だったのが、マシン性能の向上等により無視できるようになっている」、「言語の進化に伴い問題だった箇所は改善されている」など、今では事実上意味の無いルールになっていないか?)
- ルールに従うことで、可読性を犠牲にしてはいないか? (特に、ルールを修正することで可読性向上が見込まれるのであれば、改正を検討すべき)
- 他の言語系で決められたルールを、無批判にそのまま移行して使っていないか?
等々。
◇「途中return禁止」ルール
もともとのガード節に関するツイートに対し、「出口が複数あって却って見づらい」旨の否定的な意見を受けて。
途中リターンの是非についてはこの辺りがいいかな。http://t.co/8DxWeuyMSJ そういう意味では「MISRA-Cの呪縛」って呼んだ方が正しいかもしれない。ここには書かれてないけど、実は途中リターンはMISRA-C:2012では撤廃されているのもポイントね!
— たいたい竹流 (@torgtaitai) 2015, 2月 14
ここで注意すべきなのは、構造化プログラミングの文脈では、「1つの入り口と1つの出口を持つ、順次・選択・反復の3つの論理構造の組み合わせ」というスタイルはプログラム実行時のフローをソースコードから追いやすくするために用いられている、という点だ。
MISRA-Cにおける「関数の末尾以外の return 禁止」の真意 - 新・日々録 by TRASH BOX@Eel
関数内での途中returnは、大抵がネストを浅くする――つまり静的構造自体が複雑化することを避ける目的で使用されることが多いはずだ。構造化プログラミングでは考慮されていない部分なのだ。
こうなってくると、基本的には「1つの入り口と1つの出口を持つ、順次・選択・反復の3つの論理構造の組み合わせ」というスタイルをとりつつも必要に応じてルールを破る、というスタイルに落ち着く。
MISRA-Cにおける「関数の末尾以外の return 禁止」の真意 - 新・日々録 by TRASH BOX@Eel
◇「goto禁止」ルール
上述した「一度のみ実行するループを用いた方法」に関して、そういった(自分でも自覚しているような)トリッキーな方法を使うのではなく、gotoを使うべき、という意見。
- 「gotoを禁止」するルールによって、かえって可読性が損なわれてしまうのであれば、gotoを使うべき(上記他、多重ループから抜けるケースなど)であり、ルールの例外を許容すべき
- (移植などの関係で、言語間でなるべく記述方法の差異をなくしたい、という意見に対して)言語毎に適切な違うイディオム・作法があるため、極論すれば手法統一するという方針は無理がある
タイムリーな記事もあった。
C言語の開発者によるgoto文の使い方を対象とした実証研究の結果、「goto文は無害だと考えられる」 - エキサイトニュース200万近いC言語のファイルと1万1千件を超えるプロジェクトからランダムに抽出した統計的に有効なサンプルを質的および量的に分析したところ、開発者はほとんどの場合gotoの使用を適切に制限しており、Dijkstra氏が懸念したような無制限な使用は行われていないことが判明した。これらのことから、実際にはgotoは有害でないものと考えられる。
「goto一律禁止」のルールで作られているものはそもそもgoto文が現れないので抽出されないだろうし、そうでない場合は思考停止すること無くよく考えて使う開発者の割合が高くなるような気もするので、この結果を鵜呑みにして実際に解禁したら無制限な使用が増える、という可能性も否定できない気はするけれど。
*1:自分の場合は最初見た例がdo{…}while(false)だったために割とすんなり理解できたし、その土台があったため、警告対策でfor(;;){…}に切り替えた際も特に違和感を感じなかったという経緯がある