Brian Kernighanの『hoc』インタープリタから学ぶUNIXの哲学

はじめに

プログラミングの教科書では、計算機を作る課題がよく取り上げられます。これは、単純な式の解析や逆ポーランド記法の解説のための課題で、かんたんな式のパーサを実装して話が終わることが多いものです。しかし、Brian KernighanとRob Pikeによる「UNIXプログラミング環境」に登場する「hoc」(high-order calculator、高機能電卓の略)は、そこからさらに一歩踏み出した魅力的な例となっています。

今回は、この小さくも洗練されたプログラミング言語「hoc」の特徴と、それが体現するUNIX哲学について考えてみましょう。

hocインタープリタとは

一般的な計算機プログラムの作成課題で計算できる式は、

(3 + 4) * 5

のようなものです。

しかし、hocは単なる計算機ではありません。例えば、以下のようなコードを「計算」できます:

func square(x) {
    return x * x
}
for (i = 1; i <= 10; i = i + 1) {
    if (i % 2 == 0) continue
    print i, square(i)
}Code language: PHP (php)

数学的な表現をそのまま書けるだけでなく、変数、関数、条件分岐といったプログラミング機能も備えています。このようにシンプルな構文でプログラミングができるため、小さな言語でありながら、驚くほど表現力があるのです。

C言語のパワーを活かした設計

hocの内部実装で特に興味深いのは、C言語の関数ポインタを直接使用している点です。現代のプログラミングの感覚ではちょっと「行儀の悪い」ようにも感じますが、初期のC言語の特徴を活かした非常にエレガントな解決策でした。

シンボルテーブルと関数ポインタ

hocの実装で特に印象的な部分を見てみましょう。

C言語のパワーを活かしたhocの設計 hocの内部設計 C言語の低レベル機能と 高レベル抽象化の融合 関数ポインタの活用 • C言語の関数ポインタを直接使用 • 初期C言語の特徴を活かした設計 • エレガントな解決策 シンボルテーブル設計 • 関数名と関数ポインタの対応付け • 異なる要素を統一的に格納 • unionによる柔軟な型対応 /* 組み込み関数のシンボルテーブル */ static struct { char *name; /* 関数名 */ double (*func)(); /* 関数ポインタ */ } builtins[] = {“sin”, sin, “cos”, cos, …}; /* 関数ポインタの実行 */ double execute_function(Symbol *s, double arg) { return (*(s->u.func))(arg); } /* ポインタを取り出して直接実行 */

以下は関数をシンボルテーブルに登録する部分のコードです:

/* 組み込み関数のシンボルテーブルへの登録 */
static struct {
    char *name;      /* 関数名 */
    double (*func)();  /* 実際の関数ポインタ */
} builtins[] = {
    "sin",  sin,
    "cos",  cos,
    "atan", atan,
    "log",  log,
    "log10", log10,
    "exp",  exp,
    "sqrt", sqrt,
    "int",  (double (*)())integer,
    "abs",  fabs,
    0,      0
};
Code language: JavaScript (javascript)

このコードは、C言語の関数ポインタを直接使ってhocの関数テーブルを構築しています。関数名の文字列と実際のC言語の関数へのポインタを対応付けるシンプルな表です。

関数が呼び出される際には、関数ポインタを直接呼び出すコードが実行されます。例えば:

<code>double execute_function(Symbol *func_symbol, double arg) {
    return (*(func_symbol->u.func))(arg);
}</code>Code language: HTML, XML (xml)

このようなコードでは、シンボルテーブルに保存された関数ポインタ(func_symbol->u.func)を取り出し、引数(arg)を渡して実行しています。

シンボルテーブルは、関数や変数の情報を保持する構造体の配列やリストとして実装されます。

typedef struct Symbol {
    char *name;     /* シンボル名 */
    int type;       /* 変数、関数、定数などの種類 */
    union {
        double val;        /* 変数の場合は値 */
        double (*func)();  /* 関数の場合は関数ポインタ */
        /* その他の型 */
    } u;
} Symbol;Code language: JavaScript (javascript)

このようなuniomを使った設計により、シンボルテーブルに異なる種類の要素(変数、関数など)を統一的に格納でき、sin、cos、logなどの数学関数を簡単に組み込めるようになっています。C言語の強力な低レベル機能を活かしながら、高レベルな抽象化を実現している点は、Kernighanの天才的なところといえるでしょう。

Yaccによる宣言的な文法定義

また、hocのパーサ部分ではYacc(Yet Another Compiler Compiler)を使った宣言的な文法定義が使われています:

expr    : NUMBER            { $$ = $1; }
        | VAR               { $$ = Lookup($1); }
        | VAR '=' expr      { $$ = Define($1, $3); }
        | FUNC '(' expr ')' { $$ = (*($1->u.func))($3); }
        | expr '+' expr     { $$ = $1 + $3; }
        | expr '-' expr     { $$ = $1 - $3; }
        | expr '*' expr     { $$ = $1 * $3; }
        | expr '/' expr     { $$ = $1 / $3; }
        | '(' expr ')'      { $$ = $2; }
        ;
Code language: PHP (php)

この例は、数学的表現をどのように解析するかを定義しています。各行は文法規則を表し、中括弧内のコードはその規則が適用されたときの動作を定義しています。たった数行のコードで、数式の計算方法を完全に定義しているのです。

段階的に成長する言語

「UNIXプログラミング環境」の中では、hocは最初、単純な計算機から始まります。そこから徐々に変数や関数、制御構造などの機能が追加され、本格的なプログラミング言語へと成長していく過程が示されます。

この段階的な発展が非常に自然で、プログラミング言語の本質を学ぶのに最適なのです。

第1段階:基本的な計算機(hoc1)

  • 四則演算(+, -, *, /)
  • 括弧を使った優先順位の制御
  • 例:(3 + 4) * 5

第2段階:変数の導入(hoc2)

  • 変数への値の代入
  • 変数の値の参照
  • 例:x = 10
  • 例:y = x * 2

第3段階:組み込み関数と制御フロー(hoc3)

  • 数学関数(sin, cos, log, exp, sqrt など)
  • 比較演算子(<, >, <=, >=, ==, !=)
  • if-else による条件分岐
  • while によるループ
if (x > 0) {
    y = sqrt(x)
} else {
    y = 0
}Code language: JavaScript (javascript)

第4段階:ユーザー定義関数(hoc4)

  • 関数の定義と呼び出し
  • 引数の受け渡し
  • 戻り値
func square(x) {
    return x * x
}
a = square(5)Code language: JavaScript (javascript)

第5段階:プログラミング機能の強化(hoc5)

  • for ループ
  • break, continue 文
  • delete 文(変数や関数の削除)
  • print 文(値の出力)

例:

for (i = 1; i <= 10; i = i + 1) {
    if (i % 2 == 0) continue
    print i, square(i)
}Code language: HTML, XML (xml)

この発展過程が魅力的なのは、各段階で必要最小限の機能だけが追加され、無駄がないことです。また、前の段階で作ったものをそのまま拡張しているため、プログラムの成長が自然です。

例えば、hoc1で式の解析器を作り、hoc2ではそれに変数処理を追加し、hoc3ではさらに制御構造を追加する、といった具合です。これは、小さなプログラムから始めて徐々に機能を拡張していくUNIXの開発手法そのものを体現しています。

各段階での実装は比較的小規模ですが、最終的には科学計算や簡単なシミュレーションにも使えるほど強力な言語に成長します。このように、段階的な開発アプローチは、複雑なプログラムを理解しやすい形で構築する方法を教えてくれるのです。

UNIXの哲学が凝縮された章

興味深いことに、hocのようなインタープリタの開発はそれだけで一冊の本になりそうなテーマなのに、Kernighanはそれを「UNIXプログラミング環境」の中の一章として扱っています。

この配置自体がUNIXの哲学を表しています。「小さなツールを組み合わせて大きな仕事をする」という考え方です。実際、hocはYaccとLexという構文解析ツールを使って作られており、少ないコードで強力な機能を実現しています。

「作れる」という感覚

hocの最も重要な点は、読者に「自分にもこういうものが作れる」という感覚を与えることでしょう。UNIXの世界では、プログラマ自身がツールを作って問題を解決することが奨励されています。

この「作れる感じ」こそが、C言語とUNIXの本流を表しています。シンプルな設計、低レベル操作の自由度、高い表現力—これらがhocの設計にも現れていて、数ページの説明でも理解できるようになっています。

まとめ

Brian Kernighanの「hoc」インタープリタは、単なるプログラミング例題以上の価値があります。それは、プログラミングは難しいことではなく、誰でも学べる創造的な活動だという信念を体現しています。小さなツールから始めて、徐々に機能を拡張していく設計思想は、今日のオープンソース文化やDIY精神の源流となっているのです。