名前を、おぼえる言語

よみもの+手を動かす時間:およそ30分

電卓は、おぼえられない

レッスン4で、電卓は完成しました。でもこの電卓には、決定的にできないことがあります。おぼえることです。

x = 10 と教えようとすると、どうなるか。完成したはずの言語に、話しかけてみてください。

① ことばの粒(トークン)
x名前==10
② 構造の木

まだ木になりません — この章では、まだ「変数」は登場していません。

③ 評価された値

この章では、まだ「変数」は登場していません。変数は、もうすこし先のレッスンであなたが実装します。

①を見ると、x は「名前」という粒として読めています。困っているのは②——いまの文法には「名前に値をおぼえさせる」という文のかたちが、そもそもないのです。忘れる以前に、受け取ってもらえません。

言語に、記憶を

今日のレッスンで言語が育つと、こうなります。2行のプログラムを話しかけるのは、これが初めてです。

① ことばの粒(トークン)
x名前==10x名前*計算2
② 構造の木
  • プログラム
    • 代入x =
      • 10
    • かける ×
      • 名前x
      • 2
③ 評価された値
1020

②の木に、新顔が2ついます。1行目の「x =(代入)」と、2行目の「x(名前)」です。③には 10 と 20——各行が残した値が、順に並んでいます。

1行目で教えた 10 を、2行目が使えています。前の行のできごとが、次の行まで残っている。言語に、記憶が生まれました。

使う側から、かける側へ

コース1のレッスン2で、あなたは「値に名前をつける魔法」を使う側にいました。今日は机の反対側に座りなおして、あの魔法をかける側から見ます。

種明かしを先に言います。言語が名前をおぼえる仕組みは環境(environment)と呼ばれ、その中身は「名前と値の対応表」が1冊あるだけです。

国語辞典を思い浮かべてください。「庭」を引けば意味が出てくるように、環境で「x」を引けば 10 が出てくる。ただし辞書とちがって、環境はプログラムが1行進むたびに書きかわります——読むための本ではなく、書きこみながら使う帳面です。

記憶の正体は、Mapが1冊

JavaScriptには、対応表のためのデータ構造がはじめから用意されています。Map です。環境の実装は、本当にこれだけです。

JavaScript
// 環境=名前と値の対応表。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か所だけです。

JavaScript
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
③ 評価された値
100100200
ヒント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か所。代入は表に書く、名前は表を引く
  • 知らない名前を引かれたとき、何と答えるか——それもエラー設計のうち
  • 「箱」はどこにも作られていない。「あだ名」のたとえを、実装が裏づけた