プログラミング言語のはなし 1. 制御構造を中心に

(これは1993年度の授業用に書いたもので、多少古くなっていますが、 いくつかの訂正以外はそのまま収録しています。)

プログラミング言語についての概論を、補足を含めて書いておきたい。 ただし、これは、わたしの個人的な主観をまじえているところもあり、また 資料を調べないで記憶に頼って書いているところがある。 事実関係のまちがいに気づいたらぜひ訂正していただきたい。

現在実用に使われている大部分の計算機は、フォンノイマン(von Neumann)の 計算機モデルに従っている。 (このモデルに従った計算機ができる前は、計算機のプログラミングとは、 配線をつなぎ変えることだったようである。) フォンノイマン型計算機では、計算機の記憶装置(メモリー)には、演算装置の 処理対象となるデータのほか、演算装置に対する(機械語)命令を記述した プログラムも置くことができ、プログラムとデータとの間に厳密な区別がない。 (したがって、実行中に自分自身を書き変えるプログラムも作ることができるの だが、その後のソフトウェア工学の発展のなかで、フォンノイマン型計算機上でも プログラムとデータの区別をすることが勧められるようになってきた。) メモリーには順番にアドレス(番地)がついている。演算装置は、機械語の1つの 命令を実行すると、原則として次のアドレスにある命令を実行する。 このような順次処理だけで記述できるプログラムには限りがある。 そこで、機械語にはジャンプ(jump)または「go to」と呼ばれる命令が用意されて いる。これは、次に実行すべき命令の存在するアドレスを指定するものである。 これを使うと、同様な仕事をくりかえすループを構成することができる。ただし 無限ループだけでは困るので、ある比較を行なって答が真になったときだけ実行 される条件つきジャンプ命令も用意されている。

この種の計算機が現われた初期には、人が機械語でプログラムを入力していたが、 次に、機械語の命令やプログラムで利用するアドレスに名前をつけ、多少人間に わかりやすくした「アセンブリ言語」というものが発明された。アセンブリ言語を 機械語になおすのがアセンブラである。アセンブラの仕事はほぼ1対1の命令の置き かえである。

アセンブリ言語よりももう少し人間側の需要に近い表現で命令を与えることを くふうしようと考えるのは当然の成りゆきである。

ここで流れは2つに分かれる。機械語のように計算機が処理すべき命令の手順を 指定するか、処理の結果が満たすべき条件だけを記述して手順は計算機に(実際は ソフトウェアに)任せるかである。人間側の需要は後者なのだが、プログラム言語 の発達は前者のほうが早かった。計算機の能力がやらせたい仕事に比べて弱かった ので、人間が順序立てをしてやる必要があったのである。 この立場を、よく「手続き型言語」というが、「手続き」ということばが 別の意味に使われていてまぎらわしいので、わたしは「手順立て型言語」と 呼ぶことにしている。「命令型言語」と呼ぶ人もいる。 手順立て型でない言語の例としては、ものごとどうしの「つながり」を扱うLisp、 論理命題を扱うProlog、関係データベース(結合された表の集まり)を扱うSQLなどが ある。また、表計算ソフトウェアの利用も、手順立て型でない計算機利用と言える (123.exe などというプログラム自体は手順立て言語で書かれているに違いないが)。

FortranもPascalもCも、手順立て型言語であり、基本的に同じ系統の言語である。 (別に述べる予定だが、Basicもそうである。) 設計された目的が、Fortranは数値計算、Pascalは情報科学教育(とくにコンパイラを 作るような人向けの)、Cは(1960年代にはアセンブラがカバーしていた)オペレー ティングシステムの記述やハードウェアの制御、という背景を持っているために、 教科書に取り上げられる例文にはそれぞれに偏りがある。 しかし、これらのうちのある言語で書けることは他の言語にも(多少の「外来語」 の導入が必要なことがあるが、原則としては)ほぼ1対1に翻訳できるのである。

(ちなみにわたしは、Fortranになじんでいるので、PascalやCは、頭の中でFortranに 訳しながら理解している。一方、余りに手順立て言語になじんでしまったので、 Lispなどが使えないのはもちろんであるが、表計算ソフトも「表入力ソフト」として しか活用できなくなってしまった。意欲のあるみなさんには、手順立ての道に染まり 過ぎないうちに他のプログラミングの考え方も経験することを勧める。)

まず、1960年ごろに、数値計算向きのFortranと、事務計算向きのCOBOLの2つの 言語が代表的なものとして確立した。 ここで数値計算というのは、実数の近似値としての浮動小数点数 (「aかける10のb乗」)を使ったかけざん、わりざんを多く含んだ演算である。 計算機内部では2進の浮動小数点数であり、人間が読み書きする10進の浮動小数点数 と厳密に同じではない。計算を有限桁数で行なう以上いろいろな誤差はあるので、 この変換に伴う誤差もやむをえないとしたのである。 事務計算というのは役所や会社の会計処理とか、国勢調査の集計のようなもので、 (アメリカでいえば)1億ドルに対して1セントの桁もまちがいがないようにしたい。 そこで10進の固定小数点数をおもに使うように設計された。

なお、1970年代まで、計算機へのデータ入力手段は主としてパンチカードであり、 出力手段は主としてラインプリンタ(1行単位で印字するプリンタ)であった。 パンチカードの、1枚80桁(つまり1行80字)、大文字と小文字の区別なし、という 制約が、FortranやCOBOLの言語スタイルに大きな影響を与えている。

FortranもCOBOLも、プログラムの制御の記述のしかたは、フォンノイマン型計算機 モデルの考え方を引き継いだ。つまり、プログラムは変数宣言部と実行部に 分かれるが、計算機はプログラムの実行部の文を最初から順次実行していく。 ただし「go to」命令で行き先の文のラベル(Fortranの場合は文番号)を指定する ことができ、これが現われると、計算機は行き先のラベルのところから実行を 続けることになる。

FortranやCOBOL、あるいはアセンブラによるプログラミングの経験を積むにつれて 「go to文は有害である」ということがわかってきた。プログラムの上にとんだり、 下にとんだりすることを無制限に認めると、制御の流れがからみあって、 プログラムのまちがい探し(俗に「デバッグ=虫とり」という)や機能追加のときに 苦労する。こういうプログラムを「スパゲッティ」と評する人もいる。 機械語レベルでジャンプ命令を使わないわけにはいかないが、人間が読み書きする プログラム言語では一定の構造の文だけを使うべきだ、ということが、 1970年代の10年くらいかけてソフトウェア工学の常識となってきた。

プログラムの構造化に関係する問題としては、

などのことがあるが、(b)以下は別の機会に述べることにし、ここでは制御構造の 問題に限っておく。オランダのダイクストラ(Dijkstra)や、 Pascal言語を考案したスイスのヴィルト(Wirth)、Cの原典の本の共著者であると ともに、Fortranプログラムを添削した「プログラム書法」の共著者でもある アメリカのBell研究所のカーニハン(Kernighan)などの、それぞれニュアンスは 違うが、共通に主張しているところである。

BohmとJacopiniの構造化定理というものがあって、 要するに、フォンノイマン型計算機の機械語で 書ける制御構造は、「順次処理」「条件別処理」「ループ」の3つの構造の 組み合わせで書き直すことができるのである。 ただし、ここで組み合わせというのは、入れ子構造を含む。たとえばループの 中に他のループがあったり、条件別処理があったりしてもいいのである。 1970年代に作られたPascalやCでは、初めからこの考え方を表現する構文を 持っている。FortranやCOBOLもしだいにこの方向に改良されてきた。

(ここでFortranとしては1977年版標準(Fortran 77)にもとづいて述べる。 Fortranの標準は1990年、1995年に改訂されているが、 これに対応するコンパイラをわたしは使っていないので残念ながら紹介できない。)

手順立て言語では、順次処理は単に文の次に文を書けばいい。

細かいことを言うと、Fortranでは文と文を区切るのは(原則として) 改行であり、PascalやCではセミコロン(;)、COBOLではピリオド(.)である。 (さらに細かいことを言うと、PascalとCとでセミコロンの位置づけはちょっと違う。 例をあげると、if文のelseの前にセミコロンをPascalでは打たないがCでは打つ。) PascalやCでは、プログラムの改行は空白文字と比べて特別な意味を持たない。 人間にとって読みやすいかどうかだけの役割を持っている。 FortranやCOBOLが80桁のカードの影響をひきずっているのに対して、 PascalやCは行の長さを(無限ではないが)自由に選べるオペレーティングシステム で作られたことの反映だろう。

条件別処理の標準的構文は「if 条件 then 文グループ1 else 文グループ2」 である。「else」のところは英語としては「otherwise」のほうが自然なのだが、 字数が多いのが嫌われて「else」になったらしい。「条件」のところは、真か偽か の値をとる論理式である。条件が真のとき文グループ1が、偽のとき文グループ2が 実行される。もちろん一方の文グループが空でもよい。

入れ子構造を認めているので、文グループ1や文グループ2の中に順次処理を記述する 必要が生じる。この場合の文法の設計は大きく2通りある。1つは、複数の文を1つに まとめて扱うための構文を導入する。PascalやCがこの方法をとっている。 もう1つは、条件別処理やループを記述する構文の中に、複数の文の順次処理を 記述できるようにしておくことである。ここではFortranの場合を紹介する。

Pascalで「BEGIN」で始まり「END」で終わるブロック、Cで「{」で始まり「}」で 終わるブロックは、それぞれの言語で1つの文が来るべきところに置くことができる。 したがって、PascalのIF構文は「IF 論理式 THEN 文1 ELSE 文2」とだけ定義して おけばよい。Cの場合は「if (論理式) 文1 else 文2」である(THENを書かない)。 改行は自由に入れていいのだが、わたしは、次のように、多少行数がふえても、 BEGINとENDの桁がそろっているスタイルがいいと思う。

Pascal:
      IF 論理式 THEN
      BEGIN
        文1; 文1'
      END
      ELSE
      BEGIN
        文2; 文2'
      END;
C:
      if (論理式)
      {
        文1; 文1';
      }
      else
      {
        文2; 文2';
      }
FortranのIF構文は次の「ブロックIF」構文を基本と考えたほうがよいだろう。
      IF (論理式) THEN <改行>
        文グループ1 <改行>
      ELSE <改行>
        文グループ2 <改行>
      END IF <改行>
THENとELSEの間、ELSEとEND IFの間はそれぞれ複数の文を(改行で区切って)含める ことができる。END IFがブロックIF構文の終わりを示す。 変形として
(a)
      IF (論理式) THEN
        文グループ1
      END IF
(b)
      IF (論理式1) THEN
        文グループ1
      ELSE IF (論理式2) THEN
        文グループ2
      ELSE IF (論理式3) THEN
        文グループ3     ...  ELSE IF 文はこのようにいくつあってもよい
      ELSE
        文グループx
      END IF
(c)
      IF (論理式) 文
などがある。(c)はブロックIF構文導入以前からあったIF文で、文が1つだけ 来るときに使える。(この場合はTHENを書いてはいけない。)

ループについては、ある条件が満たされている限り繰り返す、というのを基本に 考えよう。「WHILE (条件) 文グループ」という表現がよく使われる。Pascalでは 「WHILE 論理式 DO 文」、Cでは「while (論理式) 文」である。

Pascal:
      WHILE 論理式 DO
      BEGIN
        文1; 文1'
      END
C:
      while (論理式)
      {
        文1; 文1';
      }
Fortranでは(77標準に関する限り)残念ながらこのようなループの構文がない。 次のようにgo toを(ループを作ることとループからの脱出に限定して)使うことに しよう。なお、行番号は左端から5桁めまでに、本文は7桁め以降に書く。
Fortran:
 9000 IF (論理式の否定) GO TO 9900
        文1
        文1'
      GO TO 9000
 9900 CONTINUE
ただし、次のような(Fortran 90の)構文がすでに使えるコンパイラもある。
Fortran 90:
      DO WHILE (論理式)
        文1
        文1'
      END DO
Fortranを設計した人は、このような一般的なループではなく、回数の決まった 繰り返しを表現するループ構文を作ってしまった。こういう需要は多くて、また 機械語に翻訳するときこの構造であることがわかっていると実行効率のよいもの にすることができるからである。ループを回るごとに、制御変数の値を、順に 一定値(指定しなければ1)だけふやしながら、初期値から終端値まで変えていく。
Fortran:
      DO 1000 k = 1, 100, 10
        文グループ
 1000 CONTINUE
77標準までのDO文では文番号が必要だが、90標準の次の形はすでに多くの コンパイラで利用可能である。
Fortran 90:
      DO k = 1, 100, 10
        文グループ
      END DO
この処理内容は、もしWHILE構文があれば次のように書くこともできる。
      k = 1
      DO WHILE (k .LE. 100)   注意: 刻み(この例では10)が負なら .GE.
        文グループ
        k = k + 10
      END DO
WHILEがなくてもGO TOで書くこともできるが、勧めない。

同様なループは(少し制限がきびしいが)Pascalにも、また(少し一般化されすぎて while文に比べたメリットが少ないが)Cにもある。

Pascal:
(Fortranのように刻みを指定できず、刻み1とみなされるので、実質的に
 上のFortranの例文と同じ意味にするためにIF構文を重ねてみた。)
      FOR k := 1 TO 100 DO
      BEGIN
        IF k MOD 10 = 1 THEN
        BEGIN
          文グループ
        END
      END
C:
(初期設定・くり返し条件・次の回への変更処理を自由に設定できる。
この例では同じ制御変数を使っているが、その必要はない。)
      for ( k = 1;    k <= 100;    k += 10 )
           初期設定  くり返し条件  次の回への変更処理
      {
        文グループ
      }
(なお、k += 10 は k = k + 10 と同じ。)
以上のような構文を入れ子で使えば、(それ以外の)go to文は、いらないはずである。 実際には、例外的事態の処理などで、go to文を使ったほうがむしろきれいに書ける こともある。(さらに新しいプログラム言語では、例外処理文を追加して、go toの 必要性をさらに減らしているらしい。)


1996-06-21; 2000-04-18; 2003-05-29
増田 耕一

目次へ