Goto 文を使った繰り返し

さて、色々あって前回の記事から一年ほど開いてしまいました。
その間に地震があったり、Small Basci のバージョン1.0がリリースされたりと様々な事が起きました。
Small Basci 日記、再開したいと思います。

前回、For 文を使った繰り返しについて説明しました。For 文は予め繰り返しの回数や条件がはっきりしている場合にとても便利に使えますが、逆に条件がはっきりしない、不定だったり、そもそも回数を決めないで繰り返しを行いたい場合には別の分を使ったほうがよい場合があります。
そこで今回はGoto 文を使った繰り返しの方法を説明したいと思います。

Goto 文は、処理の流れを変えるための命令です。

通常プログラムの処理の流れは書かれた順に沿い、プログラムの最初から最後に至るまで順番に実行されます。これはある意味戯曲の台本や、音楽の楽譜の流れと同じです。ただ、以前に紹介したように通常プログラミング言語には処理の流れを変える方法、言い換えれば命令が備わっています。それが、前回までに説明した For 文による繰り返しであり、If文による条件分岐です。

Small Basci にはFor 文やIf 文以外にも処理の流れを変えるための命令がいくつかあり、Goto 文はその中の一つです。

Goto 文は、英語の"Go to"、つまり「〜へ行け」という文に由来します。Goto 文はパラメータとして「ラベル」を一つ指定してやる必要があります。ラベルとはプログラマーが自由に定義することのできるキーワードで、Goto文での飛び先を指定します。

Goto label     <<-----(1)
  y = 10       <<-----(2)
label:
  y = 20       <<---- (3)
 x = 60       <<-----(4)

この例では(1)でラベル"label"を指定してGoto文が支持されています。そのため、Goto文が実行されると、次に実行されるのはGoto文の次の行、(2)の命令ではなく、ラベル"label"で指定された飛び先の次の行、つまり(3)からとなります。(3)が実行された後は、通常通り(3)の次の行、(4)が実行され、そのまま順に処理が続きます。
ところで、ラベル"label"はお尻にコロン(:)をつける必要があることに注意してください。これは、このキーワードがラベルだということを表すための、まぁ約束事のようなものです。ラベルは、Goto での飛び先の目印だと覚えておいてもらえばいいと思います。

でも、処理の流れを変えるとなにが嬉しいのでしょうか? Goto 文はとても自由度が高い、ある意味強力な命令です。色々な使い方ができるのだけど、特に「ループ(繰り返し)」と、「大域脱出」に使われることが多いと思います。「大域脱出」についてはちょっと難しくなるので、これは別の機会に説明したいと思います。なので、今日は「ループ(繰り返し)」について説明しようと思います。

次の例を見てください。

main:                          <---- (1)
  Shapes.Move(bar1, x1, y1)  <---- (2)
  Shapes.Move(bar2, x2, y2)    <---- (3)
Goto main                      <---- (4)
Program.End()                  <---- (5)

ラベルは自由にプログラムの中のどこにでも置くことができます。それは、そのラベルを指定しているGoto文の前であっても後ろであってもかまいません。ただし、同じ名前のラベルを複数置くことはできないので注意してほしい。Goto で飛ぶときに、どちらのラベルに飛べばよいのか分からなくなるからね。

さて上の例の場合、(4)の位置のGoto 文が(1)の位置のラベル(main)を指定しています。この場合、(2)、(3)と実行してから(4)のGoto文で(1)に実行が移るので、結果もう一度(2)、(3)が実行されます。そのあともう一度(4)のGoto文に来るのだけど、またまた(1)のラベル(main)に飛んで、(2)、(3)が実行されて、再度(4)のGoto文に来るのだけど、この流れが永遠に繰り返されることがわかるだろうか?
つまり、(1)と(4)で囲まれた命令が永遠に(無限に)繰り返されるわけです。このような処理の流れを「無限ループ」と呼ぶことがあります。
じゃぁ、この繰り返しはいつ終わるのだろうか?答えは、決して終わらない、です。もっと正確に言えば、外部からプログラムが停止されない限りプログラムは永遠に実行され続けます。外部からプログラムを停止するには、テキストウィンドーが開かれている場合はCtrl-C(Ctrl キーを押しながら c を押す、もしくはグラフィックウインドーが開かれている場合はウィンドウーの閉じるボタン(×ボタン)を押すとよいでしょう。または、Small Basic の実行ウィンドーの中の「プログラムの終了」ボタンを押してもよいです。

では、ちょっと例として前回作ったランダムに色を変えて円を描くプログラムを無限ループさせてみましょう。前回の例ではFor 文を使って100回という回数を切ってループさせていましたが、これを永遠に実行し続けるように改造します。どこを変えたかは、以前のサンプルプログラムと比較して調べてみてください。

WIDTH=640
HEIGHT=400
RR=50
GraphicsWindow.Width=WIDTH
GraphicsWindow.Height=HEIGHT
loop:
  x=Math.GetRandomNumber(WIDTH)
  y=math.GetRandomNumber(HEIGHT)
  r=math.GetRandomNumber(RR)+20
  GraphicsWindow.BrushColor=GraphicsWindow.GetRandomColor()
  GraphicsWindow.FillEllipse(x,y,r,r)
Goto loop

これを実行すると、こんな感じ。


モンテカルロ法

さて、ちょっと脱線してみましょう。

今、縦横それぞれ2mの正方形が地面に描かれているとして、そこにたくさんの小石をばらまいて見ることを想像してください。小石は正方形の中に特にどこかに隔たることなく、満遍なく程よく乱雑にばらまかれるとしましょう。さて、次にこの正方形の中に、ぴったりと入る半径1mの円を描いてみましょう。この場合、この円の中に入っている小石の数は、元の正方形の中の小石の数に比べてどの程度入っていることが期待できるでしょうか?

まだ数学で「確率」や「統計」というのをちゃんとは習っていないかもしれないけれど、直感でも正方形、および円の中に入っている小石の数の比はそれぞれの面積の比に近いものになるのじゃないか、と想像できると思います。そして、その想像はそう間違っているわけではないのです。

ちなみに、
正方形の面積: 2 \times 2 = 4 m^2
円の面積: \pi \times 1 \times 1 = \pi m^2

仮に、正方形全体に入った小石の数をa、円の中に入った小石の数をそのうちのbとすると、次の式が成り立つはずです。ちなみに\simeqは≒と同じ意味で、ほぼ等しいという事を表しています。これは、この式が確率試行を基にしているからです。
 a:b \simeq 4:\pi
a\pi \simeq 4b
\pi \simeq 4b/a

これは何を意味しているかというと、地面に描いた正方形と円の中に小石をばらまくことにより、円周率πの値、少なくともそれに近い値を求めることができるのではないか、という事なのです。では、小石をたくさん集めて実験をしてみましょう。ただ、実際には小石を100個投げるのも、一個の小石を100回投げるのも本質的な違いはないはずだから、たくさんの小石を集める手間を考えると、一個の小石を何回も投げてみるほうが効率はよいでしょう。

でも、本当に何百回も小石を投げてみるのか…?

こういう単純な作業を延々と繰り返すのはコンピュータの得意とするところなので、コンピュータにやらせるのはよいアイディアと言えるでしょう。とは言っても、実際に小石を投げる必要はない。仮想的な小石で十分です。ということで、サンプルプログラムが以下。

offset = 50
r = 500
cx=r/2
cy=r/2
inCount=0
count=0

GraphicsWindow.Width = r + offset * 2
GraphicsWindow.Height = r + offset * 2
GraphicsWindow.Show()

GraphicsWindow.DrawRectangle(offset,offset,r,r)
GraphicsWindow.BrushColor="Pink"
GraphicsWindow.FillEllipse(offset, offset, r,r)

Loop:
  x = Math.GetRandomNumber(r)
  y = Math.GetRandomNumber(r)

  distance = Math.SquareRoot((x-cx)*(x-cx)+(y-cy)*(y-cy))
  If(2*distance < r) Then
    inCount = inCount + 1
  EndIf
  count = count + 1
  GraphicsWindow.SetPixel(x+offset, y+offset, "Black")
  pi=4*inCount/count
  
  TextWindow.CursorLeft=0
  TextWindow.CursorTop=0
  TextWindow.Write("trial "+count+" ")
  TextWindow.WriteLine("PI="+pi)
Goto Loop

これを実行すると黒枠の正方形とピンクの円を描き、その中に小石(点)を打ってゆきます。背景にうっすらとピンク色の円が見えるでしょうか?

ちょっと補足しておくと、円の中心の座標つまりこのサンプルプログラムでは(cx, cy)から点(x,y)までの距離を求めるには距離の公式をつかうとよいでしょう。
 Distance = \sqrt{(x-cx)^2+(y-cy)^2}
距離の公式をもしまだ習っていないのであれば、ピタゴラスの定理を思い浮かべるとよいでしょう。

上は約500,000回小石を投げたに相当する試行を行った結果なのだけど、ではシミュレーションの結果はというと、例えば次のようになります。

概ね、3.14に近い値になっているのがわかるかと思います。50万回も試行して3桁程度の値しか求められないのでちょっとがっかりかもしれないけど、このようなコンピュータを使ったシミュレーションの手法を「モンテカルロ法」と言います。モンテカルロとはカジノで有名なモナコ公国の地区の名前です。つまり、カジノでサイコロを振るようにしてシミュレーションをするところから名づけられた手法なのです。モンテカルロ法は解析的に計算で求めることができないような数値計算の代わりに近似値を求めるためのシミュレーションとして実際に工学等で使われている方法です。

このサンプルプログラムはイメージが湧きやすいようにグラフィックスを使ってどう石が投げられているかを視覚的に見せたけれども、シミュレーションそのものには別にグラフィックスは必須ではありません。また、正方形の中の円を考えるよりも、正方形の中の一つの角を中心とすると1/4円の扇型を考えたほうがプログラムはシンプルになります。なぜなのか、は自分で考えてみると面白いと思います。

色々と最適化してみたのが次のサンプルプログラムです。

r = 1000
rr=r*r
inCount=0
count=0

Loop:
  x = Math.GetRandomNumber(r)
  y = Math.GetRandomNumber(r)

  If((x*x+y*y) < rr) Then
    inCount = inCount + 1
  EndIf
  count = count + 1
  pi=4*inCount/count
  
  TextWindow.CursorLeft=0
  TextWindow.CursorTop=0
  TextWindow.Write("trial "+count+" ")
  TextWindow.WriteLine("PI="+pi)
Goto Loop

このサンプルでは敢えて平方根を求めるMath.SquareRoot()を使うのを避け、二乗のまま比較をするようにしています。何故かというと、一般にコンピュータは平方根を求めるのは非常に時間がかかるので、平方根の計算をするのを避けているためです。とは言っても、これはほかの演算、例えば乗算と比較しての事なので実際にはそれなりに高速に計算します。もう一つの理由としては、平方根を求めることにより演算の誤差が増えていくことを避けるということもあります。

実行結果は、こんな感じ。これは100万回程試行してみた結果です。

なんだか先ほどよりも精度が悪くなった気もするけど、これはSmall Basic が返す乱数が本当の乱数ではなく数学的に求められた擬似的な乱数であることとそれにより必ずしも均質な乱数でないことなどが関係していると思います。

さて、今回のサンプルプログラムではGoto文を使った無限ループを使って実装してみました。もちろん最初から100万回試行してみる、と決めているのであればForループを使うほうが正確だし確実です。ただ、今回のサンプルの場合は最初にどのくらい試行を行うか、言い換えるとループを回すかを決めておらず、その時の気分で回してみるつもりで作ったので、まぁ、そういうわけで無限ループを使ってみました。

まぁ、そういうやり方もあるということで。

スパゲッティプログラム

Goto文を使うことで処理の流れを変えることができることが分かったと思います。けれども、処理の流れを変えるのは慎重に考えて行ってください。次のサンプルプログラムを見てください。これは、円周率πを求めるサンプルを少しいじったものなのだけど、Goto文とラベルがちりばめられたものになっています。たぶん慣れないうちは処理の流れを一つ一つ追っかけているうちに、何が何だか分からなくなってくると思います。

Goto init
main:

Loop:
  x = Math.GetRandomNumber(r)
  y = Math.GetRandomNumber(r)

  If((x*x+y*y) < rr) Then
    inCount = inCount + 1
  EndIf
  count = count + 1
  pi=4*inCount/count
  
  Goto setcusor
  label1:
  TextWindow.Write("trial "+count+" ")
  TextWindow.WriteLine("PI="+pi)
  If(count > 10000) Then
    Goto leaveLoop
Goto Loop
leaveLoop:
Program.End()

init:
r = 1000
rr=r*r
inCount=0
count=0
Goto main

setcusor:
TextWindow.CursorLeft=0
TextWindow.CursorTop=0
Goto label1

このようにGoto文で処理の流れがあっちに行ったりこっちに行ったりするプログラムの事をスパゲッティプログラムと呼ぶことがあります。スパゲッティって、麺がお皿の中でこんがらがってて、一本の麺を最初から最後まで目で追っかけるのはとてもじゃないけどできないよね。スパゲティプログラムというのも同じで、まるでスパゲッティの麺のようにこんがらがっているところから名づけられました。

Goto文は確かに便利なのだけど、間違えた使い方をすると後で見たときに処理の流れを追いかけるのがとても大変になったりすることがあります。昔…三十年くらい前の人たちはいっその事Goto文の使用を禁止するか、文法から取り除いたほうがよっぽどいいんじゃないか、という議論をしていたことがあります。さすがにそれも極端だし、気をつけて使えば便利な事に変わりはないのだから、注意して使うようにしましょうということに結局はなりました。

それで、比較的安全でこんがらかることもない使い方としては、これは人により考え方が違うところもあるのだけど、ループと大域脱出にだけ使うのこと、と考える人もいます。

まぁ、その辺りはもう少しプログラミング慣れてから自分で考えてみるとよいでしょう。

まとめ

今日はGoto文を使って処理の流れを変えること、Goto文を使って無限ループを作る方法についてまとめてみました。
また、スパゲッティプログラムの危険性についても説明しました。