本当に初心者の人に捧げるコンピューター入門


ソフトウェア プログラム編その8

ひとまとまりの処理に名前を付ける

 前章では制御構造のことを説明しました。
 前に挙げた条件分岐とループができれば、原理的には全てのプログラムが記述可能です。ではこれでめでたしめでたしかというとそうではありません。

同じようなことがいっぱいあるな



 ちょっと昔の悪夢を思い出して頂きます。下は前の迷宮脱出プログラムの一部ですが・・・
1014  LD  260  [230]   260番地に現在位置をセットする。
1017  SUB  260  15     北の方向に隣接した位置を求める
1020  CMP [260]  1      隣接位置が壁かどうか調べる
1023  JZ  1028         (壁だった場合東方向のチェックに移る)
1025  ADD  250  1      壁でなければ250番地の内容に1を足す

1028  LD  260  [230]   同様に東方向が壁かどうか調べる
1031  ADD  260  1
1034  CMP [260]  1
1037  JZ  1042
1039  ADD  250  1

1042  LD  260  [230]   同様に南方向が壁かどうか調べる
1045  ADD  260  15
1048  CMP [260]  1
1051  JZ  1056
1053  ADD  250  1

1056  LD  260  [230]  同様に西方向が壁かどうか調べる
1059  SUB  260  1
1062  CMP [260]  1
1065  JZ  1070
1067  ADD  250  1
 よく見るとなんだか似たようなことを4回繰り返しています。5行づつブロックが4つあって、2行目と4行目はちょっと違っていますが、他は同じです。

 2行目の処理は単なる足し算で(SUBは引き算ですが−を足すと思えば同じです)足す値だけが違っており、4行目も単なるジャンプ命令でジャンプ先が違うだけです。
 考えてみたら似ているのは当たり前です。この5行ブロックは、東西南北のどこかの方向が壁であるかどうかを調べている部分です。同じような処理になって当然ですね。

 他の所にも同じようなものがありますね。おかげでプログラムが不必要に長く、読みにくくなっています。

 不必要に長く読みにくいということは、すなわち面倒くさくて間違えやすいということです。例えば壁かどうか調べるためのアルゴリズムに考え違いがあって修正しなければならなくなったとしましょう。すると同じようなところを、この場合は4カ所直さなければならなくなります。

 そこで少し考えてみてください。こういうブロックに例えばCHECKWALLとかいう名前が付けられたとしたらどうでしょう?
 すなわち

CHECKWALL(NORTH_WALL)
CHECKWALL(EAST_WALL)
CHECKWALL(SOUSE_WALL)
CHECKWALL(WEST_WALL)

とでも書いておけば、各方向の壁かどうか調べられるとしたらすごく便利なんじゃないかとは思いませんか?

 本当に便利なんです。そこで高級言語には絶対にこういう機能が付いています。
  • 実は本当の機械語にも似たような処理はありますが、特に後のパラメータの説明をしようとするとすごく分かりにくいので説明しませんでした(本当の機械語の例を参照
 当然単にCHECKWALLと書いたところで、コンピューターには何をやっていいのか分かりませんから、どこかに1回だけCHECKWALLの実際の処理を書いておく必要がありますが・・・4回書かなければ行けないのと、1回で済むのとでは大違いです。
 しかもあとから何かの理由で壁のチェックを別なところでしなければならなくなったとしても、1行追加するだけで済んでしまいます。


関数とサブルーチン



 このような機能を高級言語では関数とかサブルーチンと呼んでいます。
 サブルーチンはともかく、関数という言葉はたいていの人が覚えているでしょう?そう。あのいやな数学で出てきた

 y = f(x)

とかいったやつです。プログラム言語における関数もまあ似たようなものだと思っていて、それほど大きな間違いではありません(実はもっと広い概念なんですが)

 両者ともほとんど同じような物なのですがその違いは

関数プログラムの一部に名前を付けたもの。処理後に答えを返す
サブルーチンプログラムの一部に名前を付けたもの。処理のみで答えは返さない

 答えを返すとか返さないとか言うのはなんでしょう?
 多くの場合ある処理をするときは、結局何かの値を計算していることが多いですね。上の例では「現在位置の周囲に何本道があるか数えて」います。この場合の答えとは数えた結果の数に他なりません。

 PASCALの場合は関数を

 function 関数の名前 (パラメータ:パラメータの型):答えの型; 
 begin
   結果の計算方法を記したプログラム
   result := 何かの式;
 end;

という感じで定義します。

 ところでカッコのなかにパラメータとかいう物がありますがこれは何でしょう?

 もし関数にしたい処理がどの場所でも全く同じ処理なのであればこのパラメータは要りません。でも前の例のように、大体は似てるんだけど、ほんのちょっとだけ違うということが大半です。
 このほんのちょっとした違いだけで異なったプログラムを書いていたら大変です。

 このパラメータというのは、関数やサブルーチンにこのちょっとした違いを教えてやるための仕組み、とでも考えておけばいいでしょうか。
 ですからこの例ではパラメータは1個しかついてませんが、実はいくつでもつけることができます。

 またパラメータにしても関数の答えにしても、その実体は変数(すなわちあるメモリーの場所)です。変数である以上、以前説明した「型」を指定してやらないと困るのは分かりますね。

 PASCALでは具体例には以下のように書いたりします。

{周囲の道の数を数える関数}
 function RootCount(Position:Word):byte;
 var Count:byte;  {←関数の中だけでちょっと使いたい変数}
 begin
   Count:=0;
   if Maze[Position+D_NORTH]=Maze_PATH then Count:=Count+1; 
   if Maze[Position+D_EAST]=Maze_PATH then Count:=Count+1; 
   if Maze[Position+D_SOUTH]=Maze_PATH then Count:=Count+1; 
   if Maze[Position+D_WEST]=Maze_PATH then Count:=Count+1; 
   Result:=Count;
 end;

 これをどこか適当なところに書いておけば、プログラムの別なところで

 Y := RootCount(P); 

みたいに書くと、RootCountという関数の答え(Pという位置の周囲の道の数)がYという変数に代入されます。Pという場所の値を様々に変えてやるだけで、迷路の中のどの場所の周囲の道の数でも勘定できるわけです。

 同じくサブルーチンの場合は

 procedure サブルーチン名称(パラメータ:パラメータの型); 
 begin
   サブルーチンの処理
 end;

というような感じで書いて、プログラム中でサブルーチンの名前だけ書けばそこでそのサブルーチンの中身が実行されます。


みんな仲良く幸せに??



 このようにサブルーチンや関数というのはすごく便利な物ですが、その利点はごちゃごちゃ処理を書かなくてもいいというためだけではありません。例えば

プログラムの大枠はこれで完成した!
あとは各関数を作ればいい!
そのくらいならあいつにやらせたって構わないな!

ということができることです。

 今までのやり方でプログラムの一部を他人に任せようとしたら、一体どういうことになったでしょう?確実にひとりでやった方が速いということになります。
 でもサブルーチン化できれば、サブルーチンの動作の仕方だけをきっちり教えておくだけで他人にそこを任せることが簡単になります。

 いやそれどころか、

そういえばこういう機能の関数は既にあいつが作っていたぜ!

なんてラッキーなことさえ起こるかも知れません。
 実際、市販のコンパイラなどを買ってくれば、必ずライブラリという物が付いてきます。これは非常によく使う様々な機能を持ったサブルーチンや関数のセットに他なりません。

 そういえば前に高級言語の高級な由縁に「使い回ししやすい」と書きました。
 そう書いたのはこのサブルーチンという機能が非常に強力だからなのです。

 でも同じことなら機械語だってできるんじゃないの?と聞かれるかも知れません。確かに機械語にも少々ややこしいにしても、サブルーチン機能はついてます。しかし、様々な制限があって、高級言語でやるほど簡単には行かないんです。


サブルーチンや関数の別な利点



 サブルーチンや関数の利点はこれだけではありません。特に実際にプログラムを作る際には、これがあるとないとでは天地の差があるのです。
 
 そのためには前に迷宮脱出のアルゴリズムを考えたときのことを少し思い出してください。
 あのときはどう考えたでしょうか?

 まず最初は左手の法則を思いついて、以下のようにその法則を少し詳細に分析しました。

 1 道なりに進め
 2 分かれ道があったら
    左側に道がある場合はその道を選ぶ 
    左側に道がない場合はまっすぐを選ぶ 
 3 行き止まりになったら引き返せ 
 4 この1〜3を出口になるまで繰り返せ! 

 これだけではプログラムなんて組めないと前は説明しましたね。でももし以下のような機能の関数があったとしたらどうでしょう?

GoNext(P,D) 現在地と現在の向きから道なりの方向を計算する関数
RootCount(P) 周囲の道の数を数える関数
ExistLeft(P,D) 現在地と現在の向きから左に道があるかを判定する関数
MostLeftRoot(P,D)現在地と現在の向きから最も左の道を求める関数
GoBack(P,D) 現在地と現在の向きから後ろの方向を求める関数

 すると上のアルゴリズムが

{出口でないうちは以下を繰り返す}
 while not Posithion = EXITPOSITHION do begin
   {道なり方向に一歩進む}
   Position := GoNext(Position,Direction);
   {分かれ道の場合は}
   if RootCount(Position) >= 2 then begin
     {左側に道がある場合は}
     if ExistLeft(Position,Direction) = True then
       {左に向く}
       Direction := GoLeft()
     {そうでない場合は今向いている方向そのままでいいので
     何もしない}
 
   end;
   {行き止まりの場合は}
   if RootCount(Position) = 1 then
     {後ろを向く}
     Direction := GoBack(Position);
 end;

  • 現在地をPositionという変数であらわし、向きをDirectionという変数であらわすとします。
というようにプログラムできてしまいます!

 ちょっと待て!関数の中身がないのにプログラムと言えるか!と思われるでしょう?当然です。もちろん上のプログラムはまだ未完成です。

 でも考えてみてください。「迷路から脱出しろ」というのはひどくとりとめのない話です。それに対して例えば「後ろの方向を求めよ」というのは具体的で分かりやすく、しかも簡単な話ではありませんか?

 見ただけではどうしていいか分からないような問題でも、よく分析してみればそれより規模の小さい簡単な問題の組み合わせであったと言うのは良くある話です。科学とは問題をこういう風に分析して解決する手段です。つまり、あるややこしい問題が与えられた場合、

  1. 問題をおおざっぱにいくつかの要素に分ける
  2. 要素がまだ複雑だったら、それを更に分析していくつかに分ける
  3. これ以上簡単にならななくなるまで上を繰り返す
というような手順で解決していきますが・・・空っぽの関数によるプログラムというのは、このプロセスそのままではないですか?

  1. 問題をおおざっぱな関数やサブルーチン(空っぽ可)を使って書く
  2. 空っぽな関数やサブルーチンがあったら、もうちょっと細かい関数やサブルーチンで書く
  3. 基本的な計算式などで書けるようになるまで上を繰り返す
 逆に言うと、現在のプログラム技術というのは、与えられた問題をこのように分析・解決する事を前提に作られているといっても過言ではありません。

 じゃあこういうやり方で解決できない問題は、プログラムが書けないのか?と思うでしょう?
 その答えは はい!書けません! です。
 人が考えてどうやっていいのか分からないのだから、どうしようもありません。ここまで読んで「じゃあコンピューターに任せろ!」なんて人はもういないと思いますが・・・


前へ 目次へ 次へ