<プログラミング特論 for 慶應義塾大学理工学研究科開放環境科学専攻 M1 in 2002>

lecture 8

(本稿は Ellis Horowitz, Fundamentals of programming languages によるところが多い)

制御構造について

命令型言語のもつ基本的な制御構造は次の5種類である. これは現在ある計算機の基本的な制御構造に対応している.

最初の3命令があれば,命令型言語の制御は可能である (余談:実は一命令あればすべての実行可能なプログラムが記述できることが証明されている). サブルーチン呼び出しの機能は,一つの革命である. そして,割り込み機能はもう一つの革命であった.

高級言語の世界では,早くからモジュール化が考えられたため, サブルーチンを(プログラムの字面上で)実現することはすぐに実行された. なお,現在のプログラム言語では,通常,サブルーチンに入り口は一つである. しかし,アセンブラや Basic では複数の入り口を設けることは可能であるし, 実は,Fortran や PL/I でも可能である. しかし,これには多くの問題点が発生するため,最近では,言語に組み込まないのが普通である. これに対し,出口を複数個設けるのを許すことは普通に行われている. しかし,readability/reliability確保のため,できるだけ避けたいものである.

割り込みは,言語の機能としても捕らえられるようになったのは,Ada からである. これにより,機器組み込み型のプログラムも高級言語で記述できるようになっただけでなく, これ以降,信頼性の高いプログラムを書くためには必要な機能であると認識されるようになった.

Goto 文とラベル

goto文は,プログラム言語の歴史の中で長く険しい道を歩んできた (そしてとうとう Java では追放されてしまった.Java とて命令型言語であるにも 関わらず,である).

goto 文の形式は一般に

		 goto  < statement-label >
である(Fortran では空白は無視してくれるので,より英語らしい GO TO という書き方が許される). 文ラベル(statement-label)は,勿論,合文法的なものでなければならない. Fortran のように,ブロック構造がなく(サブルーチンを除く),再帰呼び出しが許されなく, 文ラベル(文番号であるが)がすべて局所的であるような言語においては, goto 文の意味は非常に明確である. すなわち,一つのプログラム単位の中であれば,すべての文の先頭に文ラベルを添付することが でき,ただ一つの例外を除いて,任意のところから goto 文を実行することにより, 該当する文ラベルの文へ実行を移すことができる. 言い換えれば,goto 文は機械語の無条件分岐命令を忠実に写したものである. なお,この例外とは,DO-loop (制御変数を持った繰り返し)の外部から内部への 分岐である.

Fortran では,さらに二種類の goto 文を設けた. 計算型 goto (computed goto)文と代入型 goto (assigned goto)文である. 計算型 goto 文は,

		GO TO (L1, L2, ..., Ln), Index
という形をしている. L1, L2, ..., Ln は Fortran の文ラベル(文番号)であり Index は整数型の変数である. Index の値が i の時, Li に分岐する. Index の値が範囲外の時の動作は規定されていない.

代入型 goto 文は,

		GO TO LABEL (L1, L2, ..., Ln)
という形をしている. L1, L2, ..., Ln は Fortran の文ラベル(文番号)であり, LABEL はラベル型の変数である. 実行時には LABEL には L1, L2, ..., Ln のいずれかが代入されていないといけない. ラベルの代入は特別な文 ASSIGN L2 TO LABEL で行う.

Algol60, Pascal, Ada のような(ブロック構造を持った言語では)goto 文の意味は複雑になりうる. ある個所で goto L と書かれていれば, L はその時点で知られていなければならない (Pascal ではforward reference を許すために label 文が用意されている.これは実際にラベルが現れる前に当該名がラベルであることをコンパイラに知らせる文である). L が同一のブロック内で定義されていないときには, そのブロックを含むより大きなブロックで定義されていないといけない. もしそうであれば,その goto 文を実行することはブロックからの脱出文としても解釈しないとならず, 従って,変数の割付を開放するなどの手順を踏む必要がある.

再帰呼び出しされた手続き中からの脱出が可能となると(Pascal は可能である), 意味を厳密に定義するのが面倒になるだけでなく,その処理はかなり大変なものとなる.

PL/I においては,Fortran のラベル型の概念を更に拡張して,一般の変数並みの扱いができるようにした. すなわち,比較や代入ができることはもとより,手続きへ引数として渡すことができるのである. これによって,非常に複雑な実行手順をプログラムに組み込むことが可能となったが深刻な問題も生じた. たとえば,合文法的にラベルが代入されていたとしても, 実行しようとするgoto文は,それが定義されていた有効範囲のブロック外になっている,といった事態である. 勿論,readability とか プログラムの信頼性といったものは大きく損なわれる可能性を含んでいる.

Goto 文に関する論争は E.Dijkstra が CACM に宛てた letter によって口火が切られた(``Goto statement considered harmful'', Communication of ACM, vol.11, no.3, 147-148 (1968)).彼の主張は, 「goto文は多くのプログラマによって誤用され,プログラムを読みにくく信頼性の低いものにした」というものである. この後,goto文の有用性に関する長い議論があったが特に大きな結論もなく収束していった. その過程で,goto 文がなくとも他のより構造的な制御文で代用できることが示され,また, goto文の危険性に対する一般の認識も高まった. 特に,goto 文を必要とするのは(絶対に必要という訳ではないが,ないとすると非常に効率の悪いプログラムになってしまう場合) は,結局,ループからの脱出(何かを探していて見つかった時とか,逆に見つからないことが明らかになった時,等) であることが明らかになり,その結果, exit という概念を導入すれば,効率も読みやすさ・信頼性も 犠牲にならないことが分かった.

ちなみに Java では, exit 文が用意され代りに goto 文は用意されていない. goto 文を用意しなかった最初の命令型言語は恐らく Bliss(1970年頃)であろう.

条件文(conditional statements)

Fortran の version 0 では,2種類の条件文が用意されていた.

		IF ( LogicalExp ) L1, L2		IF ( ArithExp) L1, L2, L3
第一のものは, LogicalExp の値が真であれば L1 に, 偽であれば L2 に分岐することを表す. 第二のものは, ArithExp の値が負であれば L1 に,0 であれば L2 に, 正であれば L3 に分岐することを表す. 後に,第一のものは,
		IF ( LogicalExp )   < a single Fortran statement >
の形に改められた. しかし,いずれにせよ,プログラマは,関連するプログラム部分を散らばらせるしかなく,readability と reliablity を著しく低下させることとなった.

Algol60 では

		if  LogicalExp  then  S1  else  S2  ;		if  LogicalExp  then  S1  ;
という文を導入した.S1 と S2 は複合文(複数の文からなる一つの文.ブロックより一つしたのレベル)でよいため, 複数の文を記述することができ,従って,関連した文を書き散らす必要がなくなった.

ところが,最初の Algol では,S1 や S2 に任意の文を書き連ねても良いとしたものだから, 所謂 dangling else の問題が発生した.S1 や S2 に再び条件文を記述したらどうなるであろうか.たとえば,

 		if  LogExp1  then  if  LogExp2  then  S1  else  S2  
中の else S2 は,どちらの if 文に対応しているのであろうか.

これを解決する手段の例を示す.

  • Algol60: then 節内に if 文を書くことを禁止した (書きたければ begin .... end で括ればよい)
  • Algol68: if 文の終了に fi を要求することとした.たとえば,
     if  LogExp1  then  if  LogExp2  then  S1  fi  else  S2  fi
    
  • Pascal や PL/I: else は常に一番内側の if に付属するものと解釈することにした. 従って,
     		if  LogExp1  then  if  LogExp2  then  S1  else  S2  
    
    は次のように解釈される.
     		if  LogExp1  then  ( if  LogExp2  then  S1  else  S2  )
    

実際にプログラムを作成していると次のような事態にしばしば出会う.

		if  LogExp1  then  S1
			else if  LogExp2  then  S2
				else  if   LogExp3  then  S3
				.............
					else if  LogExpn  then  Sn  else  Snn
		fi fi fi .....               fi
Ada ではちょっとした工夫ではあるが,
		if  LogExp1  then  S1
		elseif  LogExp2  then  S2
		elseif  LogExp3  then  S3
		.............
		elseif  LogExpn  then  Sn
		else  Snn
		fi
という表記法を導入した.

Algol-W 以降 case 文が導入された.例えば,Pascal では,

 		case  < case selector > of
			< case label list> :  <statement >
			    .
			    .
			< case label list> :  <statement >
		end
といった形で記述される. case 文も言語毎に良く似てはいるけれども実は全く異なるということがある. 次の点に注意すべきである.
  • < case selector > としてどの型の式が書けるか
  • < case label > としてどの型が書けるか
  • < case label > には内側からまたは外側から分岐してこられるか
  • < case label list > は相互に背反でなければならないか
  • すべての < case label list > ですべての場合を尽くさないといけないのか 「その他」に相当するラベルは用意されているか
  • 各 case の文として何が書けるか.複合文が書けるか.その部分の実行が終了したら制御は次にどこへ移るのか.

繰り返し文(iterative statements)

繰り返し(ループ)の概念は命令型言語では必須である. 繰り返しを表すのに最も単純な記号を使おうとすれば,括弧で十分である. ループすべき範囲の始めと終わりを示せばよいからである. しかし,readability をあげるため,例えば,先頭を示す括弧を loop とし,末尾を表す括弧を repeat とすることが考えられた (goto文とラベルとで implicit に表すことは避けた方が良い. ループを構成する場合はその意図(ループ)が明確であるので、他の様々な 意図から区別した方がよいのである). 勿論これでは無限ループであるので,ループを抜け出る方法が必要となる.goto文の使用がまず考えられるが,ここでも goto文の naive な使用は避けた方が良い. Bliss-10(1970年頃)では,goto 文の代りに 7 種類の exit 文を用意したが,後年, exit < label > に統一された.ここのラベルは繰り返しループにつけたラベルであり, この文は,当該ラベルのついたブロックの直後に脱出することを意味する.

while, until, そして,1 1/2 ループ

実際のプログラム作成時に多く現れるループの停止方法 (ループを継続するか否かを判断すること)に2種類ある. ループ(の中身)を実行する直前に、実行すべきか否かを判定する方法と、 ループ(の中身)を実行した後、繰り返すかどうかを判定する方法である. 前者のループ方法は多くの言語で標準的に用意されている.例えば Pasclにおいては,

		while  LogicalExp do
		< statement >
		end
となっている.これは通常 while ループと呼ばれている. 後者は,Pascal では,
		repeat
		< statement >
		end LogicalExp
となっている.これは通常 until ループと呼ばれている. 両者の違いは,until ループではループ本体が少なくとも一度は実行される ということである.

loop-repeat と exit を用いれば,while ループ及びuntil ループと等価な構造を 作ることができる.すなわち

		loop            		loop
		if not LogicalExp then exit    	< compound statement >
		< compound statement >		if LogicalExp then exit
		repeat                    	repeat

上記2者ではないがよく現れるループ構造として

		loop            		loop
		< compound statement >		< compound statement >
		if not LogicalExp then exit	if LogicalExp then exit
		< compound statement >		< compound statement >
		repeat                     	repeat
というものがある.これは時には 1と1/2ループと呼ばれるものである. これに対応する標準的な表現手段を提供している言語はみかけないが、 loop-repeat と exit を用いれば素直に表現することができる. このように loop-repeat と exit は、簡にして要をえたループ構造表現手段である.

for ループ

ループを構成する文の中で最も魅力的な生い立ちをもつものが for ループである. 初期のFORTRANからすでに DO 文という形で表され,ALGOL60 において

	
	for < v > := < init > step < incr > until < fin > do < statement >
という形が提案された.ここで(他の構造でもほぼ同様なのだが)考慮すべき課題は
  • どんな型の変数が < v > として使用可能か
  • 式 < init >, < incr >, < fin > はどこまで複雑なものが使用できるのか
  • 式 < incr >, < fin > は何回評価されるのか(第一回目のループに入る直前にただ一度評価されるのみ,ということがある).
  • 変数 < v > の値は < fin > の値と比較されるのはいつか
  • 変数 < v > はループ本体中で変更してもよいのか(index register と呼ばれる特殊なレジスタに入れるとすると、プログラマが勝手に変更するとまずいことが発生し得る).
  • ループ終了時に変数 < v > の値はどうなっているか(中途での脱出時にはどうか).
  • ループの中から外への脱出,外から中への飛び込みは許されているのか?
  • 変数 < v > のスコープはどの範囲か
言語によって保守的であったりリベラルであったりする.恐らくFORTRAN IIがもっとも 保守的であろう(FORTRANでは DOループと呼ばれる). < init >, < incr >, < fin > は整数定数でなければならないからである.なお、時代が下るに従い、制限は緩やかになっていく.

Pascalは次の二つの形を提案することにより, < incr > を+1と-1に限っている.

	for < v > := < init >  to < fin > do < statement >
	for < v > := < init >  downto < fin > do < statement >
しかし< v >として枚挙型変数を使用可能としたことにより,
	for i:= january  to december do < statement >
と書いて,1月〜12月にわたる変数 i に関するループを記述することが可能となった.

CやJavaでは極めて自由な書き方ができるのはご存じの通りである.