名前を、おぼえる言語
電卓は、おぼえられない
レッスン4で、電卓は完成しました。でもこの電卓には、決定的にできないことがあります。おぼえることです。
x = 10 と教えようとすると、どうなるか。完成したはずの言語に、話しかけてみてください。
まだ木になりません — この章では、まだ「変数」は登場していません。
この章では、まだ「変数」は登場していません。変数は、もうすこし先のレッスンであなたが実装します。
①を見ると、x は「名前」という粒として読めています。困っているのは②——いまの文法には「名前に値をおぼえさせる」という文のかたちが、そもそもないのです。忘れる以前に、受け取ってもらえません。
言語に、記憶を
今日のレッスンで言語が育つと、こうなります。2行のプログラムを話しかけるのは、これが初めてです。
- プログラム
- 代入x =
- 数10
- 式かける ×
- 名前x
- 数2
- 代入x =
②の木に、新顔が2ついます。1行目の「x =(代入)」と、2行目の「x(名前)」です。③には 10 と 20——各行が残した値が、順に並んでいます。
1行目で教えた 10 を、2行目が使えています。前の行のできごとが、次の行まで残っている。言語に、記憶が生まれました。
使う側から、かける側へ
コース1のレッスン2で、あなたは「値に名前をつける魔法」を使う側にいました。今日は机の反対側に座りなおして、あの魔法をかける側から見ます。
種明かしを先に言います。言語が名前をおぼえる仕組みは環境(environment)と呼ばれ、その中身は「名前と値の対応表」が1冊あるだけです。
国語辞典を思い浮かべてください。「庭」を引けば意味が出てくるように、環境で「x」を引けば 10 が出てくる。ただし辞書とちがって、環境はプログラムが1行進むたびに書きかわります——読むための本ではなく、書きこみながら使う帳面です。
記憶の正体は、Mapが1冊
JavaScriptには、対応表のためのデータ構造がはじめから用意されています。Map です。環境の実装は、本当にこれだけです。
// 環境=名前と値の対応表。Mapが1冊あるだけ
const env = new Map();
env.set("x", 10); // x = 10 と教えられたら、表に書く
console.log(env.get("x")); // x を見かけたら、表を引く
env.set("x", 99); // 付け替えは、ただの上書き
console.log(env.get("x"));
console.log(env.get("nazo")); // 知らない名前を引くと…?Ctrl+Enter でも実行できます
最後の行に注目してください。知らない名前を引いても、Mapは怒りません。undefined——「ないよ」という素っ気ない値が返ってくるだけです。
この素っ気なさを、言語の返事にどう翻訳するか。それが、評価器の仕事になります。
⟡ よりみち:Pythonの「環境」を、のぞき見る
Pythonの対話モードで globals() と打つと、いま生きている環境——名前と値の対応表——がそのまま表示されます。正体は dict(PythonのMap)です。あなたが今日 Map 1冊で作る仕組みを、Pythonも本当に dict でやっています。教材向けの省略ではなく、これが本物の作りなのです。
評価器の変更は、2か所
レッスン3で書いた evaluate を育てます。足すのは「名前を引く処理」と「代入で書く処理」、その2か所だけです。
const env = new Map(); // 言語の記憶
function evaluate(tree) {
if (tree.value !== undefined) return tree.value; // 数:いままで通り
if (tree.name !== undefined) { // 新顔1 名前:表を引く
if (!env.has(tree.name)) {
throw new Error("「" + tree.name + "」という名前を、まだ知りません。");
}
return env.get(tree.name);
}
if (tree.assign !== undefined) { // 新顔2 代入:表に書く
const v = evaluate(tree.assign.value);
env.set(tree.assign.name, v);
return v;
}
const l = evaluate(tree.left);
const r = evaluate(tree.right);
if (tree.op === "+") return l + r;
if (tree.op === "-") return l - r;
if (tree.op === "*") return l * r;
if (tree.op === "/") return l / r;
}
// 「x = 10」の木 →「x * 2」の木 を、順に評価する
console.log(evaluate({ assign: { name: "x", value: { value: 10 } } }));
console.log(evaluate({ op: "*", left: { name: "x" }, right: { value: 2 } }));Ctrl+Enter でも実行できます
今日いちばん大事なのは、新顔1の中の if 文です。undefined をそのまま計算に流すと、ずっと先で意味の分からない事故になります。だから入口で止めて、人間のことばに翻訳して報告する。
これは前のレッスンでやった、エラー設計の続きです。最初の実験室に戻って nazo * 2 と打つと、「『nazo』という名前を、まだ知りません。」に加えて、「先に『nazo = 値』と書いてください。」という次の一手の提案まで返ってきます。Mapの素っ気ない undefined を、そこまで耕すのが設計者の仕事です。
「箱」は、どこにも作られていない
コース1で「変数は箱ではなく、値へのあだ名」と学びました。その説明が正しかったかどうか、いまのあなたは実装を見て確かめられます。
env にあるのは「名前 → 値」の対応、ただそれだけです。値をしまう「箱」にあたるものは、コードのどこにも作られていません。hana = 100 のあとに niwa = hana と書いても、増えるのは対応表の行であって、箱ではないのです。
使う側に「あだ名」と説明されたものが、作る側では「対応表の1行」だった。たとえ話の答え合わせができる——作る側にまわった人の、これが特権です。
ひとつ、正直な予告をしておきます。記憶が生まれた瞬間から、あなたの言語は「同じ式でも、いつ評価するかで答えが変わる」言語になりました。電卓の世界にはなかった時間と順番という難しさで、この先のレッスンでずっと付き合っていく相手です。
✎ 演習:言語の記憶を、言い当てる
次の3つを打ちこむ前に、③に並ぶ値(またはエラーの文面)を予想してください。対応表が1行ずつどう書きかわるか、頭の中で再生するのがコツです。
(あ)hana = 100 niwa = hana hana + niwa の3行 (い)x = 10 x = x + 5 x の3行 (う)y * 3 の1行(y は、まだ教えていません)
- プログラム
- 代入hana =
- 数100
- 代入niwa =
- 名前hana
- 式たす +
- 名前hana
- 名前niwa
- 代入hana =
ヒント1(考え方)
代入の行も、③に値をひとつ残します(最初の実験室で 10 が見えていたのと同じです)。(い)の2行目は「右側を先に計算してから、表を書きかえる」という順番で動きます。
ヒント2(かたち)
(あ)の対応表は hana→100、niwa→100 の2行になります。同じ 100 に、名前が2つ。(う)は、このレッスンで一度見たエラー文を、名前だけ変えて思い出してください。
こたえ(の一例)
(あ)100、100、200。値は1個なのに名前は2つ——箱の絵では描きにくい光景です。(い)10、15、15。2行目は、いまの x(10)に5を足した15で、表を書きかえます。(う)「『y』という名前を、まだ知りません。」、そして「先に『y = 値』と書いてください。」という提案。
(う)であなたが予想した文面がこれと違っても、困りごとと次の一手が伝わる文になっていたなら、それも正解です。エラー文に唯一の正解はなく、書くのは言語設計者だからです。
このレッスンで分かったこと
- 変数の実装=環境。名前と値の対応表(Map)が1冊あるだけ
- 評価器の変更は2か所。代入は表に書く、名前は表を引く
- 知らない名前を引かれたとき、何と答えるか——それもエラー設計のうち
- 「箱」はどこにも作られていない。「あだ名」のたとえを、実装が裏づけた