|
構造化プログラミングは1960年代後半から1970年頃にかけて,E.W.ダイクストラ達によって提唱されたプログラミングについての考え方です。この主張は提唱当時から支持され,現在ではプログラミングにおける基本的原則として広く受け入れられています。またこの考え方は余りに基本的なものとして受け入れられている為に,この主張が意識されず当然のこととして扱われていることさえあります。また現在のプログラミング技法や手法については色々ありますが,それらは全てこの考え方の延長線上にあります。
ここではこの構造化プログラミングの考え方についての簡単にまとめてみましょう。
ダイクストラ自身の提唱は原著の日本語訳
「構造化プログラミング」ダイクストラ他(サイエンス社,昭和50年)
によって読むことができます。
構造化プログラミングは
大規模なプログラムを書くとき,どうしたら良いプログラムに仕上げることができるか。
という問いに対して与えられた回答です。
大規模なプログラムと言ったとき,どのくらいのものを指すか,色々考えられますが,ダイクストラ自身は「例えば」,「100ページの小冊子くらいのプログラム」と言っています。
50行くらいのプログラムであれば,特に構造化といったことを意識しなくてもプログラムを書くことは出来ます。一度にプログラム全体を見渡して,色々検討すれば良いプログラムにすることも出来るかもしれません。
しかし,その100倍である5000行のプログラムを書こうとすると,そのような方法ではうまく行きません。現在の本格的プログラムは恐らく数万行から数十万行から構成されていると思われますが,このようなプログラムはそれなりの基本的姿勢を定めてからでないと,良いプログラムに仕上げることは出来ません。
このような大規模なプログラムを書く為の手法が,構造化プログラミングの出発点です。他方,私達が現在念頭にある
Tiny Basic
のプログラムは高々数百行です。それでは高々数百行のプログラムを書く場合そのような手法が必要なのでしょうか?答えはYes,つまり必要ですし,有効です。即ち
構造化プログラミングは100行程度のプログラムを書く上でも有効
と言えます。構造化プログラミングは大規模プログラムのために考え出された手法ですが,それはプログラムを書く上での基本的作法,および言語の備えるべき仕様と言ったものを意味します。ですから,この態度は
小さいプログラムを書く場合でも有効ですし,考慮すべきもの
なのです。
構造化プログラミングは大規模なプログラムを,良いプログラムに仕上げるものと言いました。では良いプログラムとはどんなプログラムのことでしょう。似た問題を「プログラムとは」の項で既に一度取り上げました。そこでは
良いプログラマは正しいプログラムを書くだけでなく,
それが正しく動作することを,分かりやすく証明する必要がある
と書きました。これは実は良いプログラムと,そのまま言い換えることが出来ます。即ち,良いプログラムとは
-
正しい動作をする。
-
それが正しく動作することが分かりやすく理解できる
といえます。勿論この他に,
ということが必要です。しかし,これは少し別な問題なので,ここでは触れません。
そこで,「正しい動作をする。」と「それが正しく動作することが,分かりやすく理解できる」を考えてみましょう。「正しい動作をする」と言うのは「それが正しく動作することを理解」して始めて確認できることですから,実は殆ど同じことを述べているとも言えます。(分かりやすくということを除いて。)それでは,
正しく動作することを理解するには,どうしたら良いでしょうか?
「実行してみて,正しく動作すると確認する」ということも考えられます。しかしこの方法は,一見尤もですが,余り現実的ではありません。実際,多くのプログラムでは,起き得る全ての可能性について,試すことは不可能です。またもし,全ての可能性について容易に確認できる程度のものであれば,そもそもプログラムを書いて計算機に実行させる理由は余りありません。
とすると,
何でもって正しく動作すると確認するのでしょうか。
実行結果を見てでないとすると,それは,
プログラムの中を見て論証で確認する
のです。そしてその論証がしやすいような構造を持つプログラムを作ろうと言うのが,構造化プログラミングの考えです。つまり主張していることは,
「正しい動作をすることが分かり易く理解できるためには,
プログラムがそのような構造を持つ必要がある。」
即ち,
分かりやすい構造をもつプログラムを書こう
という主張です。勿論この主張自体は自明なことかもしれません。また,これを言うだけでは単なるスローガンでしかなく,現実的問題に役には立ちません。構造化プログラミングの主張の重要性は,そのより具体的方法を規定
しているところにあります。
次にその方法をより具体的に規定しましょう,
その方法を規定するためには,まず,私達がプログラムを理解するときに,使う道具を把握する必要があります。つまり
私達はどのような方法で,プログラムを理解しているのか,或いは理解可能なのか
ということです。
ダイクストラは上述の小論で,プログラムを理解するための道具として次の3つを挙げています。
数え上げは,具体的対象を順次列挙し,数え上げて,全ての場合を調べ,それでそのプログラムの動作を理解・確認する方法です。この方法は,プログラムに限らず,色々なものを確認する最も基本的な方法でしょう。
高校の数学の教科書でお馴染みの方法ですが,数学的帰納法は,自然数における性質を,証明する一般的方法です。ある事柄の全ての場合を尽くすのではなく,ある場合とその次の場合との関係を調べ,一般的な場合へと帰納する方法です。帰納法と言う名称ですが,論証の分類からは,「演繹法」に属します。
プログラムが扱う対象は必ずしも自然数だけではありませんが,プログラムを処理の集まりとみたとき,それは離散的な集まりで,数学的帰納法の適用できる対象です。
実際のプログラムの中では,数え上げでは場合を尽くせないとき,適用します。
例えば,繰り返しにおいて,その部分が確かに目的通り動作するかどうかの確認のときなどに適用する方法です。
これは,私達が物事を認識する場合の基本的方法ですが,プログラムを理解する上でも重要な要素です。
事柄の本質的部分とそうでない部分とを分離し,
その本質的部分を抜き出すことを抽象といいます。
しかし,実は何が本質的で,何がそうでないかを見極めるのは大変難しいことです。その意味で抽象化の方法を全て理解して,プログラミングするのは難しいことです。ですから
,そうではなくむしろ,現実的なプログラミングを通して,抽象化を理解していくと考えた方が良いでしょう。
抽象化は物事を理解する上で,大変重要なものです。しかしその方法や本質を理解するのは難しいものです。そして,構造化プログラミングを理解し,実践することで,一般的な意味でも抽象化への理解が深まるのも事実です。このような意味から,
プログラミングの勉強は単にプログラムを書くためだけでは無く,
物事を深く理解するための一つの道である
とも言えます。
次にプログラムの構造について少し考えてみましょう。まずその全体的な流れについて考えます。
プログラムはコンピュータ上で実行することを目的としたものですから,現実のコンピュータの動作状況を簡単に把握できるような構造を持つことが必要です。そしてコンピュータの動作は時間と共に進みますから,その経過をプログラム上で容易に追うことが出来るような構造を持たなければなりません。即ち,
プログラムの構造はコンピュータの時間的状態変化との明示的な対応関係をもつ必要がある
と言えます。更に
その対応するプログラム構造は,その処理の正当性を確認できる一般的枠組みを持つ必要がある
とも言えます。
コンピュータの動作から見れば,時間的経過による状態変化のみが動作状況ですが,プログラムの側からみると,動作状況はプログラムの中での対応する処理の位置と,記述されている量の持っている値です。記述されている量の持っている値とその処理の意味から,次の処理が決まります。この決めることを制御と言います。制御の仕方の構造を制御構造と言います。
ですから,コンピュータの動作とプログラムの対応関係を明示でき,その動作の正当性が制御構造で確認できるもののみを使用することが必要です。
構造化プログラミングが規定する制御構造を挙げましょう。構造化プログラミングでは次の3種類の制御構造
のみを使うと規定しています,
一番単純な関係は,プログラムの記述とコンピュータの動作経過が一致ものです。それは順構造と言われるものです。
図のように上から下へと順にプログラムが記述され,コンピュータでの処理もこれと平行的に進むものです。
この関係がプログラムとしては最も基本的なもので,この構造の正当性は,処理1,処理2,...,処理n-1,処理n
を順次確認していくことで得られます。この確認は上での道具を使えば,数え上げの方法で理解・確認されるものです。
プログラムを,入力,計算処理,出力と3つに分けたとすると,これは3つの処理からなる順構造を持つと考えられます。
プログラムを大きな枠組みで考えると,順構造を持つと考えられますが,その各々の処理の正当性を調べるとき,その処理の構造が全て順構造を持っているとは限りません。現実的処理では順構造では実現できないものもあります。
実際の問題を解決する構造は色々と考えることが出来るでしょうが,構造化プログラミングでは,プログラムの中で使ってよい制御構造を,上の順構造の他に,基本的にもう2つ
に限定します。
分岐構造は,まず,2分岐構造として,次の図で表されるものです。
左の構造は,ある条件を判断してそれが満たされれば,処理を行い,そうでない場合は何もしないで次に進みます。この構造は
〜(判断)なら,〜(処理)をする
と表現されます。
Tiny Basic の制御構造で表すと, If 〜 then〜End if 文がこれに当たります。プログラム的に書くと,
If〜(判断)then
〜(処理)
End if
と表されます。
右の構造は,ある条件を判断してそれが満たされれば,処理1を行い,そうでない場合は処理2を行い次に進みます。この構造は
〜(判断)なら,〜(処理1)をし,そうでなければ〜(処理2)をする
と表現されます。
Tiny Basic の制御構造で表すと, If 〜 then 〜 Else 〜 End if 文がこれに当たります。プログラム的に書くと,
If〜(判断)then
〜(処理1)
Else
〜(処理2)
End if
と表されます。
分岐構造としは,これらを更に一般化した,多岐分岐構造があります。これは,次の図で表されます。

これは場合に応じてそれぞれの処理を行います。この構造は
〜(場合分け判断)に応じて,それぞれ,
〜(処理1),〜(処理2),...,〜(処理n-1),〜(処理n)をする
と表現されます。
まだ説明していませんが, Tiny Basic
でもこれに対応した制御構造 Select Case 文があります。
これらの構造の正当性は,各起き得る場合それぞれについて,確認をしていく必要があります。これらの場合分けは有限通りですから,可能で,数え上げの方法で理解・確認されるものです。
繰り返し構造は次の図で表されるものです。
|

繰り返し構造1 |

繰り返し構造2 |
左図は,まず判断をします,Yes
なら処理を行い,再び判断を行います。この処理をNo
の判断がでるまで順次続けます。No
の判断が出たら,この処理を終えて,次の処理に進みます。この構造は
〜(判断)である間,〜(処理)をする
と表現されます。
Tiny Basic の制御構造で表すと, While Wend
文がこれに当たります。プログラム的に書くと,
While〜(判断)
〜(処理)
Wend
となります。For Next
文もこの構造に準じたものです。実際,
For i=a to b
〜(処理)
Next i
は
i=a
While i <=b(判断)
〜(処理)
i = i + 1
Wend
と表され,順構造と繰り返し構造の合成になっています。
右図では,まず処理を行います。その後判断を行います。それが
No ならば,処理を行います。この処理を Yes
の判断が出るまで順次続けます。この構造は
〜(判断)になる迄,〜(処理)をする
と表現されます。
まだ説明していませんが, Tiny Basic
でもこれに対応した制御構造 Do 文があります。
この構造での処理の回数は現実的条件で決まり,構造そのものからは決まりません。従ってそれらの処理を全て尽くして,それらの正当性を確認することは出来ません。この構造の正当性は次のようにして確認されます。
続いて,
この確認の方法がまさに数学的帰納法です。
次にプログラム階層化について説明します。
上で3種類のプログラムの制御構造について書きましたが,構造化プログラミングでは基本的にこれ以外の制御構造を使いません。或いは使ってはいけないと言うのが主張です。
上に挙げた以外のプログラム制御構造も考えることが出来るかも知れません。有名なものとしては
任意の処理への無条件,或いは条件付ジャンプ
が考えられます。これはいわゆる goto
文に対応します。この処理構造を認めると,いくつかの状況で,プログラミングが楽になることがあるかもしれません。しかし,構造化プログラミングでは,
敢えてこれを使わない
つまり goto文は使わない
とするのです。その理由は,この処理を認めると,
コンピュータでの計算経過をプログラム上で追うことが困難になる
ことがあるからです。このことは,処理の仕組みが分かりにくくなるだけでなく,正当性の確認を困難にさせます。
上の3種類のプログラムの制御構造については,その正当性の確認の方法が確立していますから,それに従って確認を行います。ですから,これらの範囲で書かれたプログラムは正当性の確認できるわけです。
しかし,上の3種類の制御構造だけでプログラムが書けるのでしょうか。答えはYes
です。これは,多少の制限にはなりますが,工夫すれば十分書けるのです。
実際のプログラムでは,この3種類の制御構造を,色々と組み合わせて書きますが,この組み合わせは,階層的に行われます。つまり,いくつかの処理を一まとまりと見て,それをあたかも一つの処理と見なします。そしてそのまとまりそれぞれが,3種類の制御構造のどれかになっている訳です。そしてこの各々の階層でその正当性を確認し,全体としての正当性を確認するわけです。つまり
となります。各階層での処理を調べる場合,その下の階層にある個々の処理については考えず,その全体としての処理の作用或いは機能を考えます。
このようにすれば,かなり大きなプログラムの正当性も確実に確認することができます。
この階層化の正当性は,抽象化の考えで示されます。しかし,実はむしろ逆で,このような階層化による正当性の確認をすることで,抽象化の方法の確認すると考えた方が良いかもしれません。
そこで階層構造について具体例を挙げましょう。
階層構造について,Tiny Basic
を使った簡単な例で説明しましょう。
「小さい方から100個の奇素数の表を作る。」
というプログラムを考えます。これはまず,次の1つの処理からなるプログラムと考えられます。
これは,1つの処理からなる順構造です。(処理1)が正しく動けば,このプログラムが正しい動作をすることは明らかです。
そこで,次に(処理1)を実現する方法考えます。
まず,表を格納する配列変数は与えられているとします。Tiny
Basic のプログラムとては,
Dim prime(100)
と最初に宣言したことになります。色々考えられますが,次の処理が得られたとします。
この処理を
と表わすことにします。この処理を使うと,例えば,「小さい方から3個の奇素数の表を作る。」は次のように書けます。
prime(1)=3
CalcNextPrime(1)
CalcNextPrime(2)
これと同様なものを順次続けてもプログラムを作れますが,あまりスマートではありません。そこで,繰り返し構造を使います。よく知っている
For Next 文を使いましょう。For Next
文は繰り返し構造1です。今まで説明した
Tiny Basic の文を使うと,
Dim prime(100)
prime(1)=3
For i=1 to 99
CalcNextPrime(i)
Next i
End
となります。これで,処理1が一つ下の階層の処理,更に一つ下の階層の処理へと分解されました。For
と Next
の間を処理2とします。これを図で表すと次のようになります。

さて,次に,CalcNextPrime(i)の部分を分解して,実際のプログラムにしましょう。
次の奇素数を見つけるアルゴリズムとしては,与えられた奇素数から出発して,2を順次加え,それが素数であるかどうか確かめることにします。そうするとこれは,次のように表されます。
p=prime(i)+2
While (p が素数ではない)
p = p + 2
Wend
prime(i+1)=p
(p が素数ではない)の部分は Tiny Basic
の内蔵関数 PrimeFactor を使うと簡単に実現できます。これは
(p > PrimeFactor(p))
で表されます。しかし,ここではそれを使わない方法を考えます。この場合この形だと,Tiny Basic
の今まで説明した方法では,難しいので,少し書き換えます。(この形で実現する方法を後で挙げます。)
そのために,判定のためのフラグを導入します。それをisPrimeとして,判定処理の結果,p
が素数なら,isPrime=1 ,p が合成数なら,isPrime=0
となるものとします。そうすると,上は
p = prime(i)
isPrime = 0
While isPrime = 0
p = p + 2
判定処理
Wend
とあらわされます。これは全体として順構造,でその一段下の階層に,繰り返し構造を持っている形です。
そこで次に判定処理を分解しましょう。 ここではこれは次の事実を使って判定します。
p が合成数なら,平方根 p 以下の約数がある。
p が奇数で5以上であることは分かっていますから,3,5,7,...と平方根
p
までの素数で割って判定します。それまでの素数表は既に得られているので,それを使います。このとき判定処理は次のように実現できます。
isPrime=1
j = 1
d = prime(1)
While (d <= sqr(p)) and (isPrime = 1)
If (p mod d) = 0 then
isPrime = 0
End if
j = j + 1
d = prime(j)
Wend
これも全体として順構造で,その一段下の階層に,繰り返し構造を持っている形です。更にその下の構造に2岐分岐構造を持っています。
これらを順次上の階層をに埋め込むと,最終的なプログラムとして次が得られます。(プログラム例1)
Dim prime(100)
prime(1) = 3
For i=1 to 99
p=prime(i)
isPrime = 0
While (isPrime = 0)
p = p + 2
isPrime = 1
j = 1
d = prime(1)
While (d <= sqr(p)) and (isPrime = 1)
If (p mod d) = 0 then
isPrime = 0
End if
j = j + 1
d = prime(j)
Wend
Wend
prime(i+1)= p
Next i
End
このプログラムは結果を何も表示しないので,確認したい場合は素数表を表示するプログラム
For i=1 to 100
print prime(i)
Next i
を追加して下さい。
このプログラムは最大5段の階層構造を持っていて,それらが字下げによって表現されているいます。即ち,
字下げで階層構造を明示的に表現することができる
といえます。ですから,字下げは面倒がらず忠実にすることを勧めます。
これら,各階層は,それぞれ順構造と繰り返し構造によって実現されています。このように,一見複雑に見えるプログラムも,分解・統合をしてみると上で述べた3種類の構造の組み合わせで出来ています。
この例は簡単な例ですが,このようにしていくとかなり大きなプログラムも取り扱うことが出来ます。
上では,プログラムを階層化して,各階層では下の階層の個々の処理は敢えて無視して,まとまりとしての動作のみに注意して扱ってきました。これは下の階層のまとまりを一つの部品と考え,その部品の持つ機能・性能のみに注意することに似ています。このことから下部階層の
中身が変わっても,その外部的な機能・性能が変わらなければ
プログラム全体の正当性は変わらない
ことになります。この考えを更にすすめると,下部階層の実体は別にあっても,そこへの接続口があれば良いことになります。そこで
処理のまとまりを独立させ,一つの部品としたものをモジュール
と言います。モジュールにすることをモジュール化といいます。
このモジュールの持つべき機能としては,
-
プログラムが他の部分との独立に書ける
-
特に,内部で使用する変数は局所的である
-
再帰が可能である。
などがあります。
モジュールの具体的実現方法は言語によって色々あります。関数や手続きといった名称で表されます。
モジュール化の具体的使い方は,別項「モジュール化」で説明します。ここでは局所変数のことだけ,補足的に説明します。
例えばある「モジュール1」で i
と言う変数が局所的であるというのは,別のモジュールや「モジュール1」以外のプログラム別の部分で,同じ名前の
i
と言う変数が使われていたとしても,互いに全く関係無く振舞うということを意味します。つまり,この場合,
i と言う変数は,正確には「モジュール1」での i
という変数として扱われることを意味します。
Tiny
Basic ではモジュールは関数と手続きがあります。関数は値を返しますが,手続きは値は返しません。
関数は
Function 関数名(仮引数のリスト)
...
End Function
の形で使われます。
手続きは
Sub 関数名(仮引数のリスト)
...
End Sub
の形で使われます。これらの具体的な使い方は「モジュール化」の項で説明します。
構造化プログラミングではプログラムの階層化を行うだけでなく,下部の階層を可能な限りモジュール化することを推奨しています。モジュール化を行うと
-
処理のまとまりに現われるプログラムの階層があまり深くならず,見やすい
-
モジュールごとに正当性や機能の確認ができ,保守や修正が容易になる
-
モジュールを別のプログラムに再利用することが可能になる。
のような利点があります。
次に,上に挙げた例「小さい方から100個の奇素数の表を作る。」を使ってモジュール化の例を挙げましょう。
CalcNextPrime(i)の部分を具体化するところで,
p=prime(i)+2
While (p が素数ではない)
p = p + 2
Wend
prime(i+1)=p
としました。この(p が素数ではない)の部分を関数モジュールを使って書いて見ましょう。プログラム自身は上のものと同様です。
関数 isPrime(p) は p が素数なら, 1,合成数なら, 0,を返す関数とします。但し,p未満の素数は全て表
primeに入っているものとします。関数の中で,prime(j)
を使いますから, 配列変数prime(i)
は局所変数ではいけないので,大域変数とします。そのために,Public
prime(100) を使います。
関数の部分は
Function isPrime(p)
j = 1
d = prime(1)
isP=1
While (d <= sqr(p)) and (isP = 1)
If (p mod d) = 0 then
isP = 0
End if
j = j + 1
d = prime(j)
Wend
isPrime = isP
End Function
となります。関数名に isPrime
を使ったので関数の中の判定用フラグは isP
としました。関数の最後に isP
の値を関数の返り値として設定するために, isPrime = isP
とします。これは返り値を設定の仕方で,
関数名 = 値
とします。引数は付けません。それ以外はプログラム例1の内容と同じです。
この関数の中で,j,d,p,isP
が使われていますが,これらは全て局所変数です。 prime
だけは Public 宣言で大域変数です。 isPrime
は関数名で大域変数になります。ですから,j,d,p,isP
の名前として, prime と isPrime
以外の名前であれば何を使っても構いません。
これに対してプログラム例1では全て大域変数でしたから,使われている変数名は全て違わなくてはいけません。
以下がプログラム全体です。(プログラム例2)
Public prime(100)
prime(1) = 3
For i=1 to 99
p = prime(i)+2
While (isPrime(p) = 0)
p = p + 2
Wend
prime(i+1)= p
Next i
End
Function isPrime(p)
j = 1
d = prime(1)
isP=1
While (d <= sqr(p)) and (isP = 1)
If (p mod d) = 0 then
isP = 0
End if
j = j + 1
d = prime(j)
Wend
isPrime = isP
End Function
プログラム例1とプログラム例2は同じ処理をするプログラムですが,例2の方がより構造化されています。プログラムの見方が分かっていれば,例2の方が遥かに見やすく,正当性の確認も楽です。また関数isPrime(p)は別のプログラムで使うことも可能です。
構造化プログラミングについてまとめましょう。
-
構造化プログラミングはプログラムを書く上での基本的作法である。
-
現在の色々なグ技法や手法は全てこの作法の延長線上にある。
-
大きなプログラムを書く上で構造化プログラミングは必須である。
-
小さいプログラムを書く上でもこの作法は有効
では,構造化プログラミングを学ぶにはどうしたら良いでしょうか?それは
に尽きます。ここで,構造化プログラム言語とは,構造化プログラミングが出来るような,制御構造やモジュール機能を持っている言語のことです。
Tiny Basic は一応,構造化プログラム言語
です。ですから,Tiny Basic
を使って構造化プログラミングを学ぶことが出来ます。
但し,構造化プログラミング言語は構造化プログラミングが出来るプログラム言語と言う意味です。ですから,構造化プログラム言語を使っていても,構造化プログラミングに心がけなければ構造化プログラミングを行っているわけではありません。構造化プログラミングの姿勢を身につけて良いプログラムを書くようにしましょう。
最後に構造化プログラミングの精神をスローガン風に再掲します
分かりやすい構造をもつプログラムを書こう!
|