Fortran における構造化
この記事では、Fortran が function の副作用を避ける原則を優先したため、構造化プログラミングの導入時に他言語と微妙に異なる道を進むことになったのではないか、と考察します。
1. Fortran 90 における構造化の導入
Fortran 90 では構造化プログラミングの概念が取り入れられました。後から出たぶん、初期の構造化プログラミングの理念に基づいて 1970 年前後に設計された Pascal や C のような言語に比べて進歩しており、 Pascal 後継の Modula を受けて、モジュール構造を持っていたり、制御のブロック構造の始まりと終わりが keyword ... end keyword
の形式になっていて begin ... end
や { ... }
に比べて対応が明確になっているなど、細かな点で改良が見られます。
制御構造に関して注意してみると、反復構造において do while
型の先頭で条件を評価するループはあるけれども repeat ... until
型の末尾で条件を評価するループは含まれていないことに気付きます。実際のところ、Fortran でプログラムしていると repeat ... until
型の構造を取ることは珍しく、無くても不便は感じません。さらに言えば do while
型の構造も利用頻度はさほど多くなく、多くの場合ループの途中に条件があって cycle
ないし exit
を使うことになります。
元々の構造化プログラミングの理念では、反復を継続するかしないかの条件はループの先頭か末尾に置くものとされていましたが、実際にはループの途中に条件が出てくることが多いのは、Knuth によって 1974年に指摘されていました(D. E. Knuth, "Structured Programming with go to Statements", ACM Computing Surveys, 6(4), 261-301, 1974)。
実のところ当初 Fortran 90 においては do while
構文は導入されないはずでした。しかし、最終局面で他言語にあるからという理由で規格に加えられたという経緯があります。この追加に関する背景については、以下の文献に記載があります。
- 「Using the Fortran 90 DO WHILE」(E. Reid, ACM SIGPLAN Fortran Forum, 12 (2), 13-20, 1993)
- 「The DO-WHILE Reconsidered」(C. Redwine, ACM SIGPLAN Fortran Forum, 12 (3), 15-16, 1993)
- 「On DO WHILE vs. DO with EXIT」(J. Reid, ACM SIGPLAN Fortran Forum, 12 (3), 17-18, 1993)
do while
や repeat ... until
を排除する選択は些細なことに思えますが、長年つらつら考えてきたところ実際は、 Fortran が『関数の副作用を避ける原則』を『構造化プログラミングの理念』より優先したことによる帰結だと思うに至りました。このことは深いところにある思想や原則が、実際の言語設計に影響を与え違いをもたらすことを示している例だと思います。以下に詳しく論じたいと思います。
2. 構造化プログラミング
構造化プログラミングの定義は曖昧で、俗には goto 文を使わないことだとされますが、有名なダイクストラ等の著作 『 構造化プログラミング(原題 Structured programming)』 (O. J. Dahl, E. W. Dijkstra, and C. A. R. Hoare, Academic Press, 1972)から、その内容をある程度推測することが出来ます。極言すると、この本の第一部においてダイクストラは制御の構造化について、第二部ではホーアがデータの構造化について、第三部ではダールが制御とデータを一体化させる構造化(抽象データ構造)について述べていると読めます。第三部では Simula を例にオブジェクト指向的なプログラミングを例示しています。こうしてみると goto 文排除論はあくまで制御の構造化の一面を見たのみに過ぎないと言えるでしょうし、オブジェクト指向も構造化プログラミングの一つの手法と考える方がもっともな気がします。
ところで、そもそも「構造」という言葉の意味がはっきりしないのですが、ここでいう構造とは数学で「集合に構造を入れる」という場合に似て、元々無関係であった要素(命令文、変数)間に、文法的な関係性を与えることと見ていいように思います。制御においては特定の一連の文の連なりにブロック構文を与えること、データにおいては関連する複数の変数を束ねて派生型を作ること、抽象データ構造においてはデータと操作を一体化しデータに対して許される操作を限定すること、と見ておおむね大過ないでしょう。
3. 制御の構造化
以降では制御の構造化、中でも反復構文のループに話を限定してゆきます。アルゴリズムを記述する手続き型言語では、実行文が上から下に並びの順に一文づつ実行されてゆきます(連接)、自然言語の文章と違うのは分岐と反復があって、実行すべき文が時々飛び飛びになっているところです。これは文章というより楽譜に似ています。さらに分岐と反復が入れ子になることで、急速に複雑さが増して理解が難しくなります。
制御に関する構造化プログラミングの理念では、分岐や反復をブロックとして構造化すると、プログラムはブロックを単位とする連接になって、上から下へ文章を読むように素直に分かりやすくなるとされます。また分岐や反復は少数の決まった型に還元できるとされます 『Flow diagrams, turing machines and languages with only two formation rules』 (C. Böhm and G. Jacopini, Commun. ACM, 9 (5) , 366-371, 1966)。たとえば分岐はブロック IF 構文や多重分岐構文、反復は添え字付きループ構文、ループの先頭または末尾で継続条件をみる do while や repeat...until 構文に還元されるとされます。またブロックの連接を subroutine や function としてひとまとまりに構造化することも考えられます。
しかし、先に Knuth が見出したように、ループの途中に条件を置かなければならないことが多いとするならば、無理に do while や repeat...until 型の構文に固執するより、この理念の方を修正してループの途中で exit, cycle する方に統一した方が合理的な気がします。
4. do while / repeat until のために生ずる歪み
構造化プログラミングの理念の信奉者で Pascal / Modula / Oberon の作者である N. Wirth の本にあるプログラム例を見ていると、副作用のある関数がたくさん出てきます。たとえば、ファイルから文字を読み込む I/O のところで 、疑似コードで書くと
do while (read_char(ch) /= EOF)
....
end do
というような、文字変数 ch にファイルから1文字読み込むとともに、返り値としてファイル終了条件を返すような副作用を持つ関数 read_char のような関数がよく出てきます。 そうなるのはなぜかつらつら見てゆくと do while や repeat ... until 構文は条件式が1行しか書けないので、このような副作用をもつ関数を使わざるを得ないからだということが分かります。
同様の例が、 C 言語の K&R 本で有名な Kernighan 等が書いた『ソフトウェア作法 (原題 Software tools)』 (B.W.Kernighan and P.J.Plauger, Addison-Wesley, 1976) に見られます。この本では FORTRAN66 を用いて様々な Unix 的なツールを書いて、最終的に RATFOR というトランスレーター式の構造化 Fortran 処理系を作り上げるのですが、最初の方の章で FORTRAN の文法では FUNCTION の引数を書き換えて返す副作用を持たせるのは違反ではないという注意を促して、その後副作用付の関数を基本ツールとして連発してゆきます。(私は昔ここまで読んで、気持ちが悪くなってその先を読むのをやめました。)
また初期の Pascal の言語仕様レポートをみると、最初期においては Pascal の function は引数を変更するような副作用を禁止しており、procedure は引数の変更を許していました。ところがその後の版をみると function での副作用禁止の下りが削除されていて、これら二つの違いが希薄になっています。これは function に副作用を許しておかないと、構造化プログラミングの理念を実現できないことを発見したための苦肉の策だったのではないかと推測します。
後発の C 言語では、更に進んで subroutine に当たるものを廃止し function に一元化して、function が副作用を持つことが一般的になっています。C言語はシステム記述用の言語なので、返り値を常にエラーコードにするのは合理的であると考えられますが、この場合 function は関数というより機能とでも訳した方がいいのではないかと思います。
これに対して Fortran では、1958 年の FUNCTION と SUBROUTINE が初めて導入された FORTRAN II 向けの手引書の中に、FUNCTION での引数の変更を避けるように但し書きがついています (FORTRAN II for the IBM 704 Data Processing System : Reference Manual, 1958, p.28)。最適化の観点から副作用がある場合は SUBROUTINE を使うように促しています。これは Fortran における原初からの原則とも言えると思います。実際この原則はその後も忠実に守られていて、Fortran 90 で乱数や時間に関係する組み込みルーチンが導入されたとき、これらはいずれもサブルーチンとして導入されています。これらが関数に適さないのは、rnd() + rnd() /= 2 * rnd() から明らかですが、関数型言語ブームで参照透明性とか副作用の有無に注意が向いた今では理解されやすいと思います。これに対して Pascal や C では乱数や時間を返す組み込みルーチンが関数で定義されています。
最近の言語や関数型言語などでは、文やブロックが値を返すような文法が導入されています。これによって、条件式に文を書いたり複数の文からなるブロックを書けるようになり、do while / repeat...until 型の記述の適用範囲を広げています。疑似コードで書けば、
do while (-1 /= read(9,'(a)') ch) !read 文が iostat を返すように拡張されたとして
...
end do
ようなものです。これは文が副作用を持つようなものですが、文やブロックが値を持つことは画期的な発明とされているようです。このような記述が問題にならないのは、スクリプト言語ではインタプリタによるシリアル実行が普通で最適化や並列性を考えなくて良いことや、関数型言語は実行手続きを与えるものでは無く処理系がよしなに計らうためではないかと考えられます。
一般の数学などで見られない「文やブロックが値を持つという」性質を文法にわざわざ導入するのは、計算機を主としない者にはかなり奇妙に思え、このような余分な計算機固有の知識的要請は少ない方が良いように思われます。
5. まとめと結論
以上、Fortran に do while 型の構文はあれど repeat...until 型の構文が無いことから始まって、古典的な言語では構造型プログラミングの理念に基づいてそれらの構造を活用するには副作用付きの関数が必要になること、Fortran には副作用付きの関数を避ける原則があることを述べ、Fortran はその関数副作用忌避の原則を構造化プログラミングの理念より優先したため、ループの先頭・末尾での条件判断よりも中途での条件判断を主とするようになったのであろうと推測しました。これを結論とします。
こうしてみると当初 Fortran 90 で do while 構文を排除しようとしていたのはもっともなことで、現実にそぐわない構造化プログラミングの理念に固執して、他言語に惑わされるのはいかがなものかとも思えてきます。ただ do while (条件式) と、do; if (条件式) exit では、条件式の真偽値が逆転することになるので、実際のプログラミングにおいては無理に一方のみにまとめようとせず適宜その時々に応じて分かりやすい方を選べばよいのではないかと思います。
6. 蛇足
関数型言語がブームになって参照透明性が云々かんぬん言われだす以前は function と subroutine の副作用の有無による使い分けは、なかなか理解されないものでした。今でも「subroutine は不要、function だけでいい」と言う C おじさんに出会うことがあります。ただ今回の記事を書いているうちに、構造化プログラミングの理念に忠実であろうとすると、副作用付きの関数に頼らざるを得ない事情があって、そう思うようになるのも仕方ないと分かりました。
本文内容とは離れますが、最近他言語で index 付きのループを廃して do while を使えと教える風潮があって、それは一次元結合リストに基づく、実際のデータの中身が不定長のバイト列として heap にバラバラに存在しているデータ構造に対しては適切なのですが、無批判にそれに従って Fortran の配列の様に同型・等長で連続なデータをアクセスする時まで do while を使うのを見かけることがあります。しかしそれは有害無益で、むしろ最適化を妨げるだけの自傷行為になります。「index 付きのループを廃して do while を使え」と教える人には、習う人が早とちりしないよう注意なされることを希望します。
そもそも一次元結合リスト構造では、整数の性質としてペアノの定義による「次」の存在しか使いませんが、Fortran のような直値の配列の添え字の場合、整数の環ないし整域(加減乗除[ただし商と余り])としての性質まで使ってデータの番地計算や最適化に役立てられるので、添え字付きの do loop を用いるのが適切になります。
7. 補記
この記事を書くにあたり、OpenAI の ChatGPT 4o with canvas を試しに利用してみました。下調べの段階では、ChatGPT と議論したり批判してもらったり、うろ覚えの参照文献を探してもらったりで、便利で楽しく使えました。しかし、そこでの議論を土台に本記事を書くにあたっては、こちらの主張をそのまままとめてもらいたかったのですが、主張の癖が強かったせいか望むような文章を生成しようとしないので、ほとんど全部ゼロから書くことになりました。
歴史的な事実に関しては fortran66のblog (https://fortran66.hatenablog.com) の記事をいくつか参照させてもらいました。