最近、AIの力を借りながら Zenith というちょっと変わった関数型言語を自作してみたので、その設計の面白さを紹介したいと思います!
GitHub: nezmickey/Zenith
1. Zenithってどんな言語?
一言で言うと、「ポーランド記法(前置記法)」 と 「遅延評価」 を備えた純粋関数型言語です。
でも、一番の特徴はそこじゃありません。
Zenithの核心は、 「型情報が文法の構造を決定する」 という設計にあります。
どういうことか? まずはコードを見てください。
add :: int -> int -> int;
add x y = + x y;
main :: int;
main = + + 1 2 3;
普通の言語なら (1 + 2) + 3 と書くところですが、Zenithは全部前に演算子を置きます。ここまではLispとかと同じですが、Zenithは 「優先順位のための括弧 () が原則不要」 なんです。
2. 型駆動・最短一致
なぜ括弧がいらないのか。それはパーサ(構文解析器)の中に 「型が合うまで右側を飲み込み続ける」 というルールがあるからです。
結合のアルゴリズム
-
左からトークンを読む: 最初に見つかった関数(例えば
+)を主役にします。 -
型をチェック: その関数が求める引数の型(
+ならint)と、右隣のトークンの型を比べます。 - 最短一致: 型が一致すれば、即座に引数として飲み込みます。
-
待機: 型が合わない(
intが欲しいのに右に+がある)場合、右側を先に計算させて、その結果が出るのを待ちます。
このルールのおかげで、+ + 1 2 3 は括弧がなくても自動的に (+ (+ 1 2) 3) と解釈されます。
3. 実装の裏側(高階関数と遅延評価)
実装はC++で行っています。中学生の僕だけでは難しかった部分も多かったので、AI(Antigravity)とペアプロしながら進めました。
高階関数の結合
Zenithは高階関数(関数を引数にとる関数)も、この型ルールだけでさばきます。
twice :: (int -> int) -> int -> int;
twice f x = f f x;
twice は (int -> int) 型を一番目の引数に取ります。もし右隣に * 2(部分適用されて int -> int になった関数)があれば、型がピッタリ一致するので、そのまま関数として吸い込むことができます。
遅延評価とメモ化
Zenithは遅延評価を採用しています。計算が必要になるその瞬間まで、実行を先延ばしにします。
// 実装のキモ:Thunk構造体
struct Thunk : public Value {
NodePtr node;
EnvPtr env;
bool is_evaluated = false; // メモ化フラグ
ValuePtr cached_value; // 計算済みキャッシュ
// ...
};
一度計算した結果はキャッシュ(メモ化)されるので、再帰計算も効率的です。
4. サンプルコードで見るZenithの力
用意したサンプルコードをいくつか紹介します!
① 階乗 (Recursive Factorial)
factorial :: int -> int;
factorial n = if == n 1 1 * n factorial - n 1;
if も == も全部関数です。if (n == 1) という条件分岐も、型駆動のおかげで括弧なしでスッキリ書けます。
② フィボナッチ数列 (Binary Recursion)
fibonacci :: int -> int;
fibonacci n =
if == n 1 1
if == n 2 1
+ fibonacci - n 1 fibonacci - n 2;
再帰が重なりまくっていますが、遅延評価とメモ化のおかげで fibonacci 10 くらいなら一瞬で終わります。
この書き方だとメモ化が働きませんでした! 計算量はO(2^N)でした。これから改善していきたいです。
③ 高速累乗 (Tail Recursion-ish)
pow :: int -> int -> int;
pow x n =
if == n 0 1
if == % n 2 1 * x pow * x x / n 2
pow * x x / n 2;
$O(\log n)$ のアルゴリズムです。複雑な数式もポーランド記法と型ルールで見事に構造化されます。
④ 遅延評価の証明 (The Zero-Div Test)
first :: int -> int -> int;
first x y = x;
main = first 1 / 2 0;
これ、普通の言語(正当評価)なら / 2 0 でゼロ除算エラーになりますよね?
でもZenithは 1 を返します。first の中で y(第2引数)を使わないなら、その中身は評価すらしないからです。
5. これからの展望
Zenithはまだ生まれたばかりで、やりたいことがたくさんあります!
- エラーハンドリングの強化: 今はコードが間違っていても沈黙したりクラッシュしたりすることがあるので、もっと親切なエラーを出すようにしたいです。
-
型の拡充: 今は
intとboolだけなので、double(浮動小数点)やstring(文字列)も追加して、もっと実用的なプログラムを書けるようにしたいです。 - 出力の拡充: 今は出力はmain関数の戻り値になっているため、原則一つの値しか出力できません。言語仕様の美しさを保ちながら出力の多様性も確保できる方法を考えていきたいです。
- コメント機能: 実はまだソースコードにコメントが書けません(笑)。
中学生でも、AIと一緒にプログラミング言語が作れるなんて、すごい時代だなと感じました。
これからもZenithを育てていこうと思います!
読んでいただきありがとうございました!