くりかえしと、止まらない機械
回数を、前もって知らない
コース1の「4 かい くりかえす」は、始まる前から回数が決まっていました。でも、暮らしのくりかえしには別の形があります。「お湯が沸くまで、火にかける」「誰かが出てくるまで、ノックする」——回数ではなく、条件が終わりを決める形です。
今日あなたの言語に教える while は、こちらの形です。何回まわるかは、走らせてみるまで分かりません。これが今日の主役で、あとで起きる事件の犯人でもあります。
- プログラム
- 代入x =
- 数0
- 文くりかえし
- 続ける条件
- 式くらべる <
- 名前x
- 数5
- 式くらべる <
- なかみ
- 代入x =
- 式たす +
- 名前x
- 数1
- 式たす +
- 代入x =
- 続ける条件
- 名前x
- 代入x =
読み方は「x < 5 が真であるかぎり、{ } の中を実行しつづけよ」。③の欄に並ぶのは 0 と 5——最初の代入が残した 0 と、最後の行の x の 5 で、while の文そのものは値を残しません。働くだけ働いて、黙っている文です。
実験をひとつ。x + 1 を x + 2 に変えると、x は 4 の次に 6 になり、5 ちょうどを飛びこえます。それでも止まる——条件は「5 になった瞬間」を見張っているのではなく、毎周のはじめに見直されているからです。
ふたつの変数が、いっしょに育つ
{ } の中の文は、ひとつでなくてもかまいません。1 から 10 までの和を言わせてみます。
- プログラム
- 代入goukei =
- 数0
- 代入kazu =
- 数1
- 文くりかえし
- 続ける条件
- 式くらべる <=
- 名前kazu
- 数10
- 式くらべる <=
- なかみ
- 代入goukei =
- 式たす +
- 名前goukei
- 名前kazu
- 式たす +
- 代入kazu =
- 式たす +
- 名前kazu
- 数1
- 式たす +
- 代入goukei =
- 続ける条件
- 名前goukei
- 代入goukei =
kazu が 1, 2, 3 と進み、goukei がそれを呑みこんでいって、③のいちばん右が 55。10 を 100 に書きかえれば 5050——プログラムの形は、そのままです。
見覚えがあるはずです。コース1のレッスン9で、あなたはこれとそっくりなJavaScriptを修理しました(1から5までの合計)。あのとき使う側として直した道具を、いまは作る側から見おろしています。
while を、実装する
工程②(木にする)には、もう驚かないはずです。while 条件 { 文… } という並びを読んで、「条件の式」と「本体の文のならび」を持つ節を作るだけです。問題は工程③——「成り立つかぎり」を、どう意味にするか。
手順は、声に出すと一周します。「条件を評価する。真なら本体を実行して、また条件へ戻る。偽なら、おしまい」。
// 「x < 5 のあいだ、x = x + 1」を、工程②が木にしたもの
const stmt = {
cond: { op: "<", left: "x", right: 5 },
body: { assign: "x", value: { op: "+", left: "x", right: 1 } },
};
const env = { x: 0 }; // 言語の記憶。レッスン6で作った環境
// 式を畳む:数はその数、名前は環境から、枝は左右を畳んでから
function evalExpr(e, env) {
if (typeof e === "number") return e;
if (typeof e === "string") return env[e];
const l = evalExpr(e.left, env);
const r = evalExpr(e.right, env);
if (e.op === "+") return l + r;
if (e.op === "<") return l < r;
}
// while文を意味にする。今日の主役
function execWhile(stmt, env) {
while (evalExpr(stmt.cond, env)) {
env[stmt.body.assign] = evalExpr(stmt.body.value, env);
}
}
execWhile(stmt, env);
console.log("おわったあとの x:", env.x);Ctrl+Enter でも実行できます
本体は「代入が1つだけ」と簡略化しましたが、骨組みはほんものの評価器と同じです。そして、気づいたでしょうか。あなたの言語の while を作るために、JavaScriptの while を使っています。
インタプリタのくりかえしは、実装言語のくりかえしでできているのです。実はたし算のときからそうでした——レッスン3の evaluate の中の l + r は、JSのたし算です。辞書の説明文がやはり言葉で書かれているように、言語は「すでに動く言語」の上にしか立てられません。
ではJSの while は何でできているのか。たどっていくと、機械語の「指定した場所へ戻れ」という命令に行き着きます。
止まらない機械
さて、ここからが事件です。下の実験室には、終わる条件が永遠に成り立たないくりかえしが、すでに置いてあります。
- プログラム
- 代入x =
- 数0
- 文くりかえし
- 続ける条件
- 式くらべる <
- 数1
- 数2
- 式くらべる <
- なかみ
- 代入x =
- 式たす +
- 名前x
- 数1
- 式たす +
- 代入x =
- 続ける条件
- 代入x =
プログラムがいつまでも とまりませんでした。くりかえしの「終わる条件」は、いつか本当に成り立ちますか?
1 < 2 は、いつ評価しても真です。理屈の上では、このプログラムは永遠に終わりません。なのにブラウザは固まらず、「プログラムがいつまでも とまりませんでした。」という返事が、もう表示されています。
おかしいと思いませんか。あなたの言語の while はJSの while でできているのだから、条件が永遠に真なら、JSの while も永遠にまわって、このページごと凍りつくはずです。しかもこの実験室は打つたびに実行されるので、あなたは閉じかっこを打ち終えた瞬間に、無限ループを解き放っていたことになります。
種明かし——燃料を、払わせる
種を明かします。この実験室の評価器には、**燃料(フューエル)**という仕掛けが足してあります。文をひとつ実行する、式をひとつ畳む——評価の1歩ごとに燃料を1払わせて、10万歩で尽きたら計算を打ち切り、エラーとして報告するのです。
let fuel = 10; // ほんものの実験室では 100000
let x = 0;
while (1 < 2) { // 永遠に真。本来なら、止まらない
x = x + 1;
fuel = fuel - 1; // 1歩すすむごとに、燃料を1はらう
if (fuel <= 0) { // 燃料が尽きたら、そこでおしまい
console.log("プログラムがいつまでも とまりませんでした。");
break; // くりかえしを、力ずくで抜ける合図
}
}
console.log("止まったときの x:", x);Ctrl+Enter でも実行できます
つまりさっきの返事の正体は、止まったのではなく、止められたでした。走り出すまえに防げなくても、走り出してからなら安く止められます——評価の一歩一歩が、インタプリタであるあなたの手の中を通るからです。
止め方には、もうひとつの流儀があります。コース1のレッスン9で、終わらないカウントダウンが「実行が2秒を超えたので、止めました」と打ち切られたのを覚えていますか。あれは時計で外から見張る方式で、今日の燃料は歩数を内側で数える方式です。
- 時計(タイムアウト)——どんなプログラムにも、外から付けられる。ただし速い機械は遠くまで走り、遅い機械は手前で止まる。どこで止まるかが、機械しだい
- 燃料——同じプログラムは、どの機械でも必ず同じ1歩めで止まる。ただし、評価のすべてが自分の手を通る、インタプリタの作者にしか選べない
どちらが正しい、ではありません。「止まらないプログラムは書かれうる」と認めたうえで、壊れたときの壊れ方を先に設計しておく——フェイルセーフと呼ばれる考え方です。強い道具を渡すときは、安全装置をいっしょに渡す。
「止まるか」を、先に知ることはできないのか
白状すると、燃料の報告はひとつ嘘を含みえます。「10万歩で止まらなかった」は、「永遠に止まらない」の証明ではありません。本当は10万1歩めで止まるプログラムも、同じ文言で打ち切られます。
なら、実行するまえにコードを読んで、止まるか止まらないかを正しく言い当てるプログラムを書けばいい。そう思った人は、コンピュータ科学のいちばん深い場所に、指先で触れています。
結論だけ、正直に言います。そのプログラムは、書けません。むずかしくてまだ誰も書けていない、のではなく、原理的に書けないことが証明されています——アラン・チューリング、1936年、コンピュータが完成するより前のことです。
この事実には停止性問題という名前がついています。なぜ「書けない」と言い切れるのかは、それだけでひとつのコースに値する問いなので、今日は名前だけポケットに入れて帰ってください。
⟡ よりみち:コラッツ予想——小学生に説明できて、誰も証明できない
このあと演習で書く「偶数なら半分、奇数なら3倍して1たす」には、名前があります。コラッツ予想——どんな正の整数から始めても、いつかは必ず1に着くだろう、という1937年からの未解決問題です。ルールは小学生に説明できるのに、世界中の数学者が90年ちかく証明できておらず、大数学者エルデシュは「数学はまだ、この種の問題に対する準備ができていない」と言い残しました。コンピュータは2の68乗あたりまで「ぜんぶ1に着いた」と確かめましたが、それは証明にはなりません。「まだ止まらない」と「永遠に止まらない」が別ものであるのと、同じ理屈です。
✎ 演習:止まらない機械を、止まる機械に直す
下のプログラムは「x が偶数なら半分に、奇数なら3倍して1たす」を数えようとして、未完成のまま走っています。x を付けかえる文がまだないので、燃料切れの返事が出ているはずです。2つめの ※ の行に if と else を書き足して機械を止め、27 から 1 にたどり着くまでの歩数を数えてください。
ひとつ注釈を。あなたの言語にはまだ「余り」を求める道具がないので、偶数しらべは「2ずつ引いていって、0 が残るか 1 が残るか」で代用しています。先に 100 ずつ大またで引いているのは燃料の節約で、100 は偶数なので、引いても偶数・奇数は変わりません。
プログラムがいつまでも とまりませんでした。くりかえしの「終わる条件」は、いつか本当に成り立ちますか?
ヒント1(考え方)
2つめの while が終わった時点で、amari は「x が偶数なら 0、奇数なら 1」になっています。あとは分かれ道——レッスン8で言語に教えた if と else の出番です。偶数なら x = x / 2、奇数なら x = 3 * x + 1。
ヒント2(かたち)
if amari == 0 { のまとまりの中に x = x / 2 を、} else { のまとまりの中に x = 3 * x + 1 を書いて、hosuu = hosuu + 1 の手前に置きます。
こたえ
完成すると、③のいちばん右が 111 になります。27 から出発した数は、いったん 9232 まで駆けのぼってから、111 歩めで 1 に着地します。先に x = 6(8歩)や x = 7(16歩)で小さく確かめるのも、良い進め方です。
おまけの実験をひとつ。大またの while(100 ずつ引くほう)を消して 2 ずつだけにすると、27 では「いつまでも とまりませんでした」が出ます——本当は止まるのに、です。燃料の報告が「止まらない証明」ではなく「ここで諦めたという報告」であることを、自分の言語で確かめたことになります。
偶数しらべの書き方がここと違っていても、27 で 111 が出て、その理由をあなたが説明できるなら、それが正解です。
このレッスンで分かったこと
whileは回数を前もって知らないくりかえし。意味は「条件を評価→真なら本体を実行→また条件へ」- インタプリタの while は、実装言語の while でできている。言語は「すでに動く言語」の上にしか立てられない
- 強くなった言語は止まらない可能性もいっしょに手に入れる。だから壊れ方を先に設計する——時計で見張る(タイムアウト)か、歩数を数える(燃料)か
- 「止まるかどうか」を実行せずに完全に言い当てるプログラムは、書けないことが証明されている(停止性問題)