文の列、プログラムらしさ
1行から、行の列へ
前のレッスンで、あなたの言語は名前をおぼえられるようになりました。今日は、新しい語彙をひとつも足しません。そのかわり、行を並べると何が起きるかを見ます。
- プログラム
- 代入x =
- 数10
- 代入y =
- 式たす +
- 名前x
- 数5
- 式たす +
- 式かける ×
- 名前x
- 名前y
- 代入x =
③の欄に、値が上の行から順に3つ並んでいます。1行だけのときは「計算」と呼べばすみました。でも、上の行がおぼえさせた名前を下の行が使いはじめた瞬間、この文字の列は「プログラム」と呼びたくなる何かに変わります。
今日のレッスンは、この境目の正体を、作る側の目でつかまえます。
式は「もの」、文は「こと」
コース1のレッスン6で、こう整理しました。式は値になるもの、文は起きること。机の反対側に座りなおした今、この整理はパーサのデータ構造として読み直せます。
プログラム = 文 の列(この言語では、改行が区切り)文 = 代入(名前 = 式)、または 式x = 10 は代入の文で、「x に 10 をおぼえさせる」という出来事を起こします。x * y は式だけの文で、値になる以上のことはしません。そしてプログラムとは、パーサの中では、文がただ配列に並んだものです。
②の欄をもう一度見てください。根もとに「プログラム」がいて、その下に3つの文が兄弟として横に並んでいます。式は深さで読みましたが、プログラムは列で読む——構造の見え方が、ここで一段変わります。
返事のしかたも、設計する
ところで、③に代入の値(10 や 15)まで並ぶのは、なぜでしょう。代入は出来事のはずなのに、返事をしています。これは、この言語が「各行の値を、全部見せる」という返事の設計を選んだからです。
実はPythonの対話モード——REPL(read-eval-print loop:読んで、評価して、見せる、のくりかえし)——も同じ思想です。書いたそばから返事が見えると、言語は試行錯誤の相棒になります。一方、ふだんのPythonがファイルを実行するときは、print と書いたものしか見せません。
どちらが正しいということはなく、誰に向けた言語かで答えが変わります。学ぶための言語なら、おしゃべりなほうがいい——それがこの実験室の設計判断です。
区切りがあるから、文を見失わない
この言語は、改行を文の区切りにしました。CやJavaは ; で区切ります。日本語の文が「。」で終わるのと同じで、どこで文が終わるかの取り決めがないと、読み手は文を見失います。
実験しましょう。上の実験室を全部消して、1 + 2 3 と1行に打ってください。
「文の終わりのはずの場所に「3」が続いています。」——パーサは 1 + 2 まで読み終えたあと、区切りが来るはずの場所に 3 を見つけて、そう報告します。区切りの取り決めがあるからこそ、パーサは「ここまでで1つの文」と自信を持って言い切れるのです。
⟡ よりみち:「。」と「;」——区切り記号の文化史
古代ギリシャ・ラテン語の写本には、単語の切れ目も文の切れ目もない「続け書き(scriptio continua)」の時代がありました。日本語の「、」「。」も、いまの形で広く使われだしたのは明治の終わりごろからです。区切り記号は、ことばに最初から備わっていたものではなく、読み手のために後から発明された道具——改行も ; も、その末裔です。
実行の正体は、ループ
最後に、工程③の中身をのぞきます。文の列を実行するとはどういうことか。評価器の核心は、文の配列を、上から順に回るループ——それだけです。
// 3行のプログラムを、データ(文の配列)として並べる
const program = {
body: [
{ type: "assign", name: "x", // x = 10
value: { num: 10 } },
{ type: "assign", name: "y", // y = x + 5
value: { op: "+", left: { name: "x" }, right: { num: 5 } } },
{ type: "expr", // x * y
expr: { op: "*", left: { name: "x" }, right: { name: "y" } } },
],
};
const env = {}; // 環境。すべての文が、この1つをいっしょに使う
function evaluate(node) {
if (node.num !== undefined) return node.num;
if (node.name !== undefined) return env[node.name];
const l = evaluate(node.left);
const r = evaluate(node.right);
if (node.op === "+") return l + r;
if (node.op === "*") return l * r;
}
const values = [];
for (const stmt of program.body) { // ← 実行の正体は、このループ
if (stmt.type === "assign") {
env[stmt.name] = evaluate(stmt.value);
values.push(env[stmt.name]);
} else {
values.push(evaluate(stmt.expr));
}
}
console.log(values);Ctrl+Enter でも実行できます
注目してほしいのは、env がループの外に1個だけあることです。すべての文が同じ環境をいっしょに使うから、1行目のおぼえごとが3行目まで届く。行と行をつないでいたのは特別な仕掛けではなく、共有された環境でした。
正直に言うと、「実行はただのループ」という種明かしは、拍子抜けするかもしれません。でも、この素朴さこそが今日の答えです。プログラムらしさは、実行装置の複雑さからではなく、文たちが環境を通して手をつなぐことから生まれます。
✎ 演習:3行プログラムの返事を、先に言い当てる
実験室に打ちこむ前に、次のプログラムの③の欄——3つの値と、その順番——を予想してください。それから下の実験室に打ちこんで、確かめてください。
a = 2b = a * ab + a予想が当たったら、もうひとつ。上の行で作った名前を、下の行がかならず使う3行プログラムを、自分で設計して動かしてください。
ヒント1(考え方)
③に並ぶのは「各行の値」が上から順に、でした。1行目の値は、a におぼえさせた値そのもの。2行目を評価する時点で、環境はもう a を知っています。
ヒント2(かたち)
1行目で③の1つめは 2。2行目の a * a は 2 × 2 なので、b は 4 になり、③の2つめは 4。では3行目の b + a は?
こたえ(の一例)
③は上から 2、4、6。自作のほうは、たとえば hara = 3、mori = hara + 4、hara * mori で、③は 3、7、21 になります。
あなたの3行がこれと違っていても、上の行の名前が下の行に届いて、③に3つの値が順に並んだなら正解です。名前の選び方も値も、設計者であるあなたが決めることです。
このレッスンで分かったこと
- プログラムとは文の列。パーサの中では、ただの「文の配列」
- 式は値になるもの、文は起きること。代入は「おぼえさせる」という出来事
- 実行の正体は、文の配列を上から順に回るループ。環境を共有するから、行と行がつながる
- 文の区切り方も、返事の見せ方も、言語設計者の選択