Gotoサンの話。結局のところ「goto禁止」はどういう話だったのか?

C言語で書かれた大量のソースファイルをサンプルとして、現実でのgoto文の利用方法について調査した研究の結果が発表されたようだ。

とりあえず、歴史的経緯的に非常に重要だろう指摘をしておくと、ダイクストラの「Go To Statement Considered Harmful」が出たのが1968年で、C言語の初期の設計・開発が行われたのは1969〜1973年だ。C言語は、ダイクストラの論文が大きな話題となった後に開発されている。とはいえ、まっさらな状態から設計されたのではなく、前身のB言語、BCPL、CPL、ALGOL 60と系譜を遡ることが可能である、という点には留意すべきだろう。

また、C言語のgoto文はアレでもまだ安全になった方だ。アセンブラや昔のFORTRANのGOTOの威力はC言語のソレの数十倍はあった(※オレ基準)。ダイクストラのあの論文は、アセンブラや昔のFORTRANの全盛期に書かれている。つまり、C言語のgoto文よりも強烈で野蛮なGOTOを無分別に使用することに対する懸念の表明だ。

これは私の妄想だが、天下のベル研の研究者が『Communications of the ACM』に掲載された「Go To Statement Considered Harmful」を全く読まなかったとは思えないし、その影響を全く受けなかったとも思えない。

実際、原著が1976年に出版された『ソフトウェア作法』を読んでいると、構造化プログラミングの影響を受けている印象を受ける。

もっとも、C言語の設計に「Go To Statement Considered Harmful」が影響を与えたか否かは定かではない。

そもそもダイクストラが言いたかったこと

GOTOを含む制御構文についてダイクストラが言いたかったのは、「プログラムの静的構造(ソースコード)と動的構造(実行時のフロー)の一致」だ。その背景には、「プログラムの正しさの証明」という目的がある。また結果論的だが「最初から正しいプログラムを記述する」という姿勢がある。

構造化プログラミング (サイエンスライブラリ情報電算機 32)』P.26(「構造化プログラミング論」)より:

この話の教訓として,プログラムのテキストを通じてその計算を(知的に)制御するのが私達の義務であると認めるならば,私達は,謙虚になって,“計算における進行”が“プログラムのテキストにおける進行”に直接的に写像することを保証する最も系統的な流れの制御の機構だけを用いるべきなのです.

プログラムの正しさを証明するための数学的・論理的手法は幾つか存在する。「構造化プログラミング論」では、一例として数え上げ・数学的帰納法・抽象が挙げられている。『プログラミング原論―いかにしてプログラムをつくるか (サイエンスライブラリ―情報電算機)』や『プログラミングの科学 (情報処理シリーズ)』では最弱事前条件などが出てくる。この辺+αの研究成果は、後にバートランド・メイヤーの契約プログラミング(契約による設計)に組み込まれている。

しかし、これらの手法を用いてプログラムの正しさを証明しようとした時、既に存在する「大きなプログラム*1」の正しさを証明するのは困難だった。ソフトウェア・テストは有益ではあるが、バグの存在は証明できてもバグの不在は証明できないという欠点がある。

では、どうすればよいか?

大きなプログラムの正しさを証明するのが困難な理由として、次の2つが考えられた。

  1. 証明する対象が大きいから。対象が小さければ、まだ何とかなる。
  2. 後付けで証明しようとするから。(経験的に)後で証明するのは難しい。

(1) については、大きなものを一度に全て証明しようとすることが困難であるのだから、大きなプログラムを小さなプログラムの塊に分割して、個々の小さなプログラムについて証明すればよい。ここで、小さなプログラムがサブルーティンであるならば、そのサブルーティンを呼び出す側にて、サブルーティンを「証明済みの定理」として、中身を気にせずに使用可能であるようにしておく(抽象化)。

しかし、既に存在する大きなプログラムを、改めて小さなプログラムの塊に分割するのは、プログラムを再実装しているに等しい。となれば、最初から「小さなプログラムの塊」として大きなプログラムを記述すべきだろう。

(2) については、仕様を出発点として、証明とプログラミングを並行して進める(この際、証明を若干先行させる)。「仕様に合致した仮想の計算機械」という大きく抽象的な視点から証明とプログラミングを開始し、徐々にブレイクダウンしていくことで階層化する。最弱事前条件は、証明とプログラミングを並行して進めるのに適した方法として考案された。

結果として、論理的には、プログラムを書き終えた時点で、プログラム全体の正しさが証明されていることになる。もちろんヒトは間違える動物であるので、どこかに誤りがあるだろう。しかし (2) よりプログラム全体が証明済みであるので、全体的には品質は高いはずだ。また (1) と (2) より、設計・実装の段階から「正しさの証明」を強く意識することで、証明しやすさを考慮した内部構造になっているだろうから、誤りを特定し、証明し直し、プログラムを書き直すのは比較的容易だろう。

(1) や (2) における具体的な技法が、大きなプログラムを理解可能な大きさにするための「サブルーチンによる分割統治」であり、分割統治においてプログラム全体の正しさを証明すると同時に証明しやすい構造に階層化・抽象化するためのトップダウン手法*2である。

GOTOを含む制御構文の話は、ここから先になる。GOTOの乱用が戒められた理由は、プログラムの静的構造(ソースコード)と動的構造(実行時のフロー)の乖離が大きくなるからであった。

プログラムの静的構造と動的構造が一致しているならば、ソースコードという静的構造からプログラムの正しさが証明されている時、実行時のフローという動的構造の正しさも証明されている、と考えても問題ないだろう。しかし静的構造と動的構造が一致していないならば、静的構造の正しさが証明されても、それは動的構造の正しさが証明されたことを意味しない。

何よりも、人間はプログラムの静的構造を把握することは不得意ではないが、動的構造を把握することは苦手だ(人間デバッガになった気分で、脳内でソースコードを一行ずつステップ実行しつつ、各変数の値の変遷を観察してみるとよい。絶対に、メモ書きできる何かが手元に無いと無理だろうし、それがあっても大変だ)。静的構造と動的構造が一致しているならば、無理に動的構造を把握しようと努めることはない。しかし静的構造と動的構造の乖離が大きいなら、プログラムの正確な挙動を知るためには、動的構造を把握する必要がある。だが、我々は容易に動的構造の変遷を見失ってしまう。これでは、プログラムの大まかな流れを把握することすら困難だ。正しさを証明するなど到底無理だろう。

少なくとも、アセンブラや昔のFORTRANにおける無分別なGOTOの乱用は、静的構造と動的構造の乖離を大きくするものであった。この辺り、若い人は『岩波講座 情報科学〈12〉算法表現論 (1982年)』の序章を見てみるとよい。昔のスパゲッティなコードのスパゲッティさ加減を堪能できる。

ところで、たとえ制御命令がGOTOぐらいしかないアセンブラであっても、「順次・選択・反復の入れ子」を念頭にGOTOを利用することで、随分と見通しの良いプログラムになる。ここまでで気づいた人も多いと思うが、制御構造に関しては、「静的構造と動的構造の一致」以外に、「プログラムの正しさを証明するのが容易な構造」という要素も絡んでいる。その意味では「とにかくGOTOを無くせばよい」という態度は、実に短絡的だ。

まず念のため書いておくと、ダイクストラの議論は高水準言語を念頭に置いている。

More recently I discovered why the use of the go to statement has such disastrous effects, and I became convinced that the go to statement should be abolished from all "higher level" programming languages (i.e. everything except, perhaps, plain machine code).

http://www.u.arizona.edu/~rubinson/copyright_violations/Go_To_Considered_Harmful.html

「the go to statement should be abolished from all "higher level" programming languages」、つまり「高水準なプログラミング言語からGOTOを取り除く(GOTO文を持たない言語仕様にする)べきだ」と解釈するのが妥当ではないか?

少なくとも構造化プログラミングに対する「(構造化アセンブラではない)アセンブラでは――」云々の発言は、全くの的外れだ。

次に、機械的にGOTOを取り除くことについては、ダイクストラ本人すら否定的だ。

In [2] Guiseppe Jacopini seems to have proved the (logical) superfluousness of the go to statement. The exercise to translate an arbitrary flow diagram more or less mechanically into a jump-less one, however, is not to be recommended. Then the resulting flow diagram cannot be expected to be more transparent than the original one.

http://www.u.arizona.edu/~rubinson/copyright_violations/Go_To_Considered_Harmful.html

構造化定理に基づけば、論理的にはGOTOを取り除くことが可能だ。しかし機械的にジャンプのないフローに変換することは推奨できない。なぜならば、変換後のフローが変換前のフローよりも分かりやすくならないからだ。

ダイクストラの1970年代の研究を考慮しつつ、改めて「構造化プログラミング論」を見直してみると)ダイクストラの構造化プログラミングが最終的に目指したのは、「証明とプログラミングを並行する」ことで最初からなるべく正しいプログラムを書くことであり、同時に「静的構造(ソースコード)から正しさを証明するのが容易な構造の大規模プログラム」を設計・記述することだ。例えGOTOが無くなっても、読んで理解するのが難しいソースコードのままならば、その静的構造から正しさを証明することは困難だ。

目的と手段を見誤ってはならない。あくまで目的は「静的構造と動的構造の一致」と、「プログラムの正しさを証明するのが容易な構造にすること」だ。この目的を達するための技法の1つとして、「入り口と出口を1つに制限して、順次・選択・反復の3つの制御構造を入れ子状に組み合わせる」という手法を採用している。

そして、この手法を強制しやすくするために、GOTOのようなジャンプ構文ではなく選択・反復専用の構文を使用すべきだし、それらの専用構文が揃っているならば、乱用できてしまうGOTOは言語から(あくまでもソースコードではなく言語仕様から)取り除いてしまうべきである――というのが元々の趣旨だ。

くれぐれも、現在の「goto文を用いたソースコード」ではなく、1968年当時の「GOTOバリバリの、FORTRAN等で書かれたスパゲッティなソースコード」を念頭においておくこと。四捨五入して半世紀前の話なのだから。

C言語の開発者のgoto文に対するポリシー

C言語のgoto文は、ジャンプ先がgoto文と同じ関数の中に限定されている分だけ、昔のGOTOよりは安全だ。とはいえ、それ相応の危険性があるのは間違いない。

プログラミング言語C 第2版 ANSI規格準拠』のP.79〜81を読むと、C言語の本家本元は「基本的にgotoは使うべきじゃないけど、でも2〜3の例外はあるよね」というスタンスだったことが分かる。

Cには無限に悪用され得るgotoおよび飛び先の名札も用意されている。形式としてはgotoは決して不可欠のものではない。実際にも,それを使わないでプログラムを書くのはたいていの場合簡単である。本書ではgotoは使用していない。
しかし,gotoが有用である二,三の場合をあげてみよう。(※以下略)

「gotoが有用である二,三の場合」の例として、多重ループからの脱出と、エラー処理を一箇所にまとめておく方法が挙げられている。

そして、次のように締めくくっている。

ここにあげたような少数の例外を除くと,goto文に頼るプログラムは,goto文を使わないプログラムに比べて,一般によりわかりにくく,また保守しにくい。この問題についてわれわれば別に教条主義者ではないが,goto文はとにかく滅多なことでは使うべきではないと思う。

この主張は、C言語で「品の良いgoto」を書くプログラマの肌感覚と合致しているように思う。

多重ループやループ中のswitch文からの脱出や、エラー処理を一箇所にまとめるためのgotoの利用は、モダンな言語ならば別の言語機能(例えばラベル付きbreak・continueや、try - catchなどの例外構文)で代用できるものだ。しかしC言語にはそれらの機能はない。そこでgotoで代用している。

このようなgotoの利用を観察していると、「品の良いgoto」には次のような特徴があることに気づくだろう。

  • ジャンプ先のラベルは、goto文の位置よりも後に存在する。
    • 関数の先頭側へのジャンプは滅多にない。
  • ジャンプ先のラベルは、goto文と同じブロックか、goto文を囲んでいる外側のブロックに存在する。つまり「制御構造の入れ子」の内側から外側への脱出であることが多い。
    • 興味深いことに、setjmp(3)やlongjmp(3)によるジャンプは「コールスタックの巻き戻し」であり、関数呼び出しを含む「制御構造の入れ子」の内側から外側への脱出である。

MISRA-C:2012でgoto文の扱いが変わった

さて、「日本語の解説書が出たら買おう」と待つものの未だに出る気配が無いまま今に至ってしまったMISRA-C:2012だが、最近になってルール一覧だけ見ることができた。

MISRA-C

goto文の扱いがMISRA-C:1998やMISRA-C:2004から変化している。今までは「必要:goto文を使用してはならない」のみだったが、これが「推奨」に緩和された上で、3つの細分化された要件が追加されている。

分類 項番 カテゴリ ガイドライン
ルール 15.1 推奨 goto文を使用してはならない
ルール 15.2 必要 goto文は、同じ関数のより後ろで宣言されたラベルにジャンプしなければならない
ルール 15.3 必要 goto文によって参照されるラベルは、同じブロック内、またはgoto文を囲む任意のブロック内で宣言されなければならない
ルール 15.4 推奨 繰り返し文を終了するために使用されるブレーク文またはgoto文は1個以下でなければならない

興味深いことに、ルール15.2と15.3は「品の良いgoto」を書くCプログラマのスタイルに合致している。

まとめ

とっちらかってしまったが、強引にまとめる。

  • C言語の設計・開発は「Go To Statement Considered Harmful」発表の1年後に開始している。
  • C言語のgoto文は、アセンブラや昔のFORTRANのGOTOよりはまだ安全な仕様。ただ、それと「Go To Statement Considered Harmful」に因果関係があるか否かは不明。
  • そもそもダイクストラが懸念を示したのは「昔のFORTRAN等における無分別なGOTOの乱用」に対して。その理由は、プログラムの静的構造と動的構造の乖離が大きくなることで、プログラムの正しさをを証明することが困難になるから。
  • C言語の本家本元的には「基本的にgotoは使うべきじゃない。しかし2〜3の例外はあるよね(その場合のみ使うよ)」的なスタンス。
    • 「2〜3の例外」は、最近の言語なら代用機能があるケース。しかしC言語にその機能は無いので、代わりにgoto文を使っている。
    • このスタンスは、おそらく世の中の「品の良いgoto」を書くCプログラマの賛同を得られる気がする。
  • MISRA-C:2012でのgoto文の扱いの変化は「品の良いgotoの使い方」に結構合致している。

以前「構造化プログラミング論」を読んだ時、数え上げ・数学的帰納法・抽象が出てきたのを見て何かしら引っかかるものがあったのだが、なるほど、この後のダイクストラの研究と結びつくと考えると、納得できるものがあるな。

*1:「大きな」といっても、おおよそ3000行であるので注意。『構造化プログラミング』P.2にて、巨大なプログラムとして「その大きさは,たとえばこの小論全体ほどのものです」とされているが、「構造化プログラミング論」日本語訳は32行×95ページ(1行は全角35文字/半角70文字)で、単純に考えると3040行だ。『プログラミングの科学』P.1の導入部は「大きな(3000行もの)算譜(program)を書き上げたところだ」で始まる。

*2:解こうとしている問題に合致した仮想の命令・データ型を持つ仮想の計算機械を考え、その計算機械に対して証明とプログラミングを行う。その後、段階的に仮想の命令・データ型を分解しつつ証明とプログラミングを進めていく。