文の列、プログラムらしさ

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

1行から、行の列へ

前のレッスンで、あなたの言語は名前をおぼえられるようになりました。今日は、新しい語彙をひとつも足しません。そのかわり、行を並べると何が起きるかを見ます。

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

③の欄に、値が上の行から順に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)」の時代がありました。日本語の「、」「。」も、いまの形で広く使われだしたのは明治の終わりごろからです。区切り記号は、ことばに最初から備わっていたものではなく、読み手のために後から発明された道具——改行も ; も、その末裔です。

実行の正体は、ループ

最後に、工程③の中身をのぞきます。文の列を実行するとはどういうことか。評価器の核心は、文の配列を、上から順に回るループ——それだけです。

JavaScript
// 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 = 2
b = a * a
b + a

予想が当たったら、もうひとつ。上の行で作った名前を、下の行がかならず使う3行プログラムを、自分で設計して動かしてください。

③ 評価された値
2
ヒント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 = 3mori = hara + 4hara * mori で、③は 3、7、21 になります。

あなたの3行がこれと違っていても、上の行の名前が下の行に届いて、③に3つの値が順に並んだなら正解です。名前の選び方も値も、設計者であるあなたが決めることです。

このレッスンで分かったこと

  • プログラムとは文の列。パーサの中では、ただの「文の配列」
  • 式は値になるもの、文は起きること。代入は「おぼえさせる」という出来事
  • 実行の正体は、文の配列を上から順に回るループ。環境を共有するから、行と行がつながる
  • 文の区切り方も、返事の見せ方も、言語設計者の選択