GoやTypeScriptなど、大企業がつよつよマンパワーで作ってるプログラミング言語が日の目を当たる一方で、個人が盆栽を愛でるがごとくひっそりコツコツと作っているプログラミング言語もあります。ここではGitHubのprogramming-languageトピックで見つけた自作言語の処理系から、スター数が少なくユニークな言語仕様を持った言語処理系をいくつか紹介します。
ちなみに、programming-languageトピック検索はこのようなAdvent Calendarを見る人にとって宝の山なので定期的に見ておくと幸せになれます。ただ、誰でも知ってるようなメジャー言語処理系が邪魔なので、スター数などで制限をかけておくと良いかと思います。
私は普段スター数50未満のprogramming-languageトピックが付いているリポジトリをRecently updatedで検索しています。
https://github.com/topics/programming-language?o=desc&q=stars%3A%3C50&s=updated
Melon
Daniele Rapagnaniさん作のMelonはミニマルでプロトタイプベースな動的型付き言語です。
値の構造を規定するスキームを作り、そのスキームから値を生成するアプローチをここでは仮にスキーム指向と称しましょう。そのようなアプローチの代名詞としてCの構造体やJavaのクラスがあります。一方、スキームを作るのではなく直接値を生成してしまい、必要に応じてその値を拡張していくスタイルをインスタンスベースと言ったりします。値の拡張とは素朴に値をコピーすることで実現できますが、immutableなもの、典型的には関数などは個別にコピーするのは無駄が大きいです。そこでコピーした際に共有する参照を保持しておけばいいだろうという発想に至るわけでして、その共有参照をプロトタイプ等と言います。なので、このようなアプローチをプロトタイプベースと言ったりもします。個人的にはプロトタイプはあくまで実装用語なのだから…と思いますが、連想配列をハッシュセットと呼ぶようなものでしょう。最もメジャーなプロトタイプベースはJavaScriptでしょう。MelonもJavaScriptに強く影響を受けています。
let obj = {
name = -> { return "Melon"; }
} @ {
say = -> { return "This is " .. this->name() .. "!"; }
};
io.print(obj->say()); // This is Melon
ここで、@
が値の拡張を意味します。JavaScript的に言えばObject.create
、jQuery的に言えば$.extend
です。
さて、インスタンスベースが値の構造を規定しないとは言いましたが、実際にはこんな手続き的なコードは辛いでしょう。そこで、Melonではスキーマ的な値をパスカルケースで定義し、createメソッドで値を生成するというイディオムでカバーしているようです。JavaScriptで言うところのコンストラクタですね。
let Animal = {
bark = -> { io.print(this->cry() .. "!"); }
};
let Cat = {
create = => { return { } @ Cat; }
cry = -> { return "meow"; }
} @ Animal;
let Dog = {
create = => { return { } @ Dog; }
cry = -> { return "bowwow"; }
} @ Animal;
Cat.create()->bark(); // meow!
Dog.create()->bark(); // bowwow!
ECMAScript 5th辺りの、クラス構文を使わないJavaScriptが分かる人だとなんとなく読めるんじゃないでしょうか。細かいポイントですが、->
はインスタンスメソッドを、=>
はスタティックメソッドを意味するようです。
インスタンスベースはある意味でクラスベースの内部が露出したような言語仕様ですので、どうしても手続き的な見た目になりがちです。データ構造と手続きが混ざってしまいがちで、それを嫌うとスキーマ的なアプローチになると思います。
インスタンスベースの魅力としては、魔法がない点でしょうか。手続き的なので実際の振る舞いがわかりやすい気がします。その他、よくも悪くも規約ベースなので緩く、それ故にオレオレライブラリが発達しやすいのは一昔前のJavaScriptを思い返せばわかりやすいですね。
Melonは他にもインクリメンタルGCがあったり、ASTを順にトラバーサルするナイーブな実装ではなくちゃんとバイトコード生成してVM実行していたりと実装部分も頑張ってるっぽいです(ちゃんと読んでない)。
Asteroid
Lutz Hamelさん作のAsteroidはパターンマッチング指向プログラミングと称する新しいパラダイムを掲げる意欲的な言語です。ロードアイランド大学で研究開発されてるようで、作者の方は同大学の准教授さんだそうです。大学でのプロジェクトとは言え、コミット履歴を見る限りでは個人開発っぽいですね。
Asteroidでは式を簡単にクォートできます。クォートは典型的にはLispでよく使われる機能で、式をASTとして受け取る機構のようなものです。Asteroidでは1 + 2
は3
ですが、'1 + 2
は'1 + 2
という値になります。これは要するに{ op: add, lhs: 1, rhs: 2 }
のような値を表現してると考えてください。これを実際に計算するにはeval
関数を使います。eval('1 + 2)
は3
です。
さらに、Asteroidではクォート化された式に対してパターンマッチングが可能です。つまり、let 'x + y = '1 + 2
はx
に1
が、y
に2
が束縛されます。
Asteroidは動的型付き言語なので、refutableな、つまりパターンマッチングに失敗しうるlet束縛を静的検証で弾くことができません。その場合は例外が投げられます。また、パターンマッチングをし、結果を真偽値として返すis
演算子があります。
さて、ここまで見てきたように、Asteroidはクォート化された式同士のパターンマッチングを行います。更に、クォート化された式そのものも当然普通の式です。そして、パターンをクォート化された式として表現しています。つまり、以下のようなことができます。
load "io".
let expr = 'x + y.
let *expr = '1 + 2. -- 変数束縛されたクォート化された式をパターンマッチングに使う場合、*演算子が必要
println x. -- 1
println y. -- 2
ここで、Lisp的には'1 + 2
は'(+ 1 2)
なわけですが、これがパターンマッチングを行うので、1 + 2
という数式を意味するパターンそのものと見ることもできます。その意味で、Asteroidはパターンが第一級オブジェクトであると紹介されています。
より面白い例としては以下のようなことができます。
load "io".
let pat = 'x|xs.
let *pat = [1, 2, 3, 4, 5].
println x. -- 1
println xs. -- [2, 3, 4, 5]
ここで、x|xs
はリストをheadとtailに分割するパターンではなく、headとtailからリストを構築する演算子です。例えば、1|[2, 3]
は[1, 2, 3]
です。つまり、関数を作り、式をクォートし、ユニフィケーションするという仕組みからパターンマッチングをより一般化させていると言えそうです。READMEにも書いてありましたが、いわゆるML的な意味でのパターンマッチングよりもPrologとかに近いアプローチだと思います。正直言ってAsteroidのコアはパターンマッチングではなく論理型言語のユニフィケーションだと思いますが、論理型言語を流行りのパターンマッチングという観点で再構築するのはアツいというか、Rubyにさえパターンマッチングが導入されるほど多くのプログラマに馴染み深い機能になったパターンマッチングを足がかりに論理型言語リバイバルがあるのでは?という気さえしてきました(本当か?)。
なお、|
は左結合です。なので、1|2|3|[]
とかはエラーになります。右結合の方が便利なのでは…?
ちなみに、個人的にはパターンマッチング指向プログラミングと聞くと江木 聡志さん作のEgisonを強く想起します。EgisonはML的な意味のパターンマッチングを一般化させるべく、マッチャーと呼ばれるパターンマッチング専用のモジュール機構を作る進化をしました。いずれもパターンマッチングをより多くのデータ構造に適用すべく一般化しようというゴールは同じですが、親がPrologなのかMLなのかで手段が異なった印象を受けました。Asteroidの方が仕組みとしては素直ですし汎用性もありそうですが、Egisonの方がよりパターンマッチングに特化している分、これはこのようなデータ構造に対するパターンマッチングを意図したコードなんだなということが一目瞭然というアドバンテージがありそうですね(知らんけど)。
なおAsteroidですが、研究プロジェクトのわりに簡易的なリスト内包表記っぽいものがあったり独特なオブジェクト機構があったりと趣味に生きてる感があっていい感じでした。ちなみに、アンクォート相当は見つかりませんでした。マクロもないみたいです。マクロはないのにクォートはあるって面白いですね。
Zeolite
Kevin P. Barryさん作のZeoliteはC/C++やOCamlのようにヘッダファイルがあり、宣言と定義が厳密に分離されていることが特徴の言語です。大抵の言語でのヘッダファイルというものは後方互換のためにあるものですが、新しく作った言語でヘッダファイルを導入しているのはユニークな気がします。ただし、再利用性を考慮しなければ(つまり、外部ファイルへエクスポートしなければ)ヘッダファイルは不要のようです。
Zeoliteでは宣言をconcrete
構文で、定義をdefine
構文で記述します。
concrete Program {
@type run() -> () // @typeはスタティックメソッド指定みたいなもの
}
define Program {
run() {
LazyStream<Formatted>.new()
.append("hello, world\n")
.writeTo(SimpleOutput.stdout())
}
}
宣言と定義の厳密な分離というのがミソで、Zeoliteではconcrete
なしでいきなりdefine
を書くことはできません。また、concrete
内に実装を書くこともできません。常にconcrete
とdefine
の両方を求めます。なので、単純なhello worldでさえconcrete
を書く必要があります。
上記のhello worldでいうProgram
はZeoliteではtype-categoryと呼ばれていますが、状態や関数を持つことができる点でクラスによく似ています。C++以降爆発的に普及したクラス機構ですが、C++がそうだったためか、多くの言語では宣言と定義が不可分なものとして扱われています。特に、Cからヘッダファイルを引き継いだC++でさえそうなので、いわゆるPimpl
イディオムが普及したという経緯があるわけです。つまり、C++er向けに言えば小細工なしにPimpl的な意味での実装隠蔽が可能です。
ちなみに、concrete
で関数を宣言する場合、引数名を書く必要はなく、引数の型だけを書けばいいです。また、define
で関数を定義する場合、引数や返り値の型は省略可能です。concrete
で宣言されていない関数をdefine
で定義した場合、そのtype-category内だけで使用可能なプライベート関数扱いになります。
concrete Program {
// 宣言は型にだけ集中でき
@type fib(Int) -> Int
}
define Program {
// 実装は型をもう一度書かずに済む
fib(n) {
log(n)
if (n < 2) {
return n
} else {
return fib(n - 1) + fib(n - 2)
}
}
// define内で宣言を含めた場合はプライベート関数
@type log(Int) -> ()
log(n) {
LazyStream<Formatted>.new()
.append("fib(" + n.formatted() + ")\n")
.writeTo(SimpleOutput.stdout())
}
}
ヘッダファイルには宣言しか書けないようになっています、そのため、exampleを見ていてもヘッダファイル(.0rp
)は型シグネチャしか出てこないのはスッキリしていていい感じ。ただ、実装ファイル(.0rx
)でのみアクセス可能なtype-categoryを作るためには実装ファイル内でconcrete
する必要があったり、プライベート関数があったりで、実装ファイルは宣言と定義がごちゃまぜになるのでそこはどうなんでしょうね?
その他、実装継承をなくしてinterface
だけを導入したり、型推論があったり、型変数や変性の指定ができたり、合併型や交差型をかんたんに作れたりするそうです(ちゃんと試してない)。
総括
正直言って、GitHubにある大抵の言語処理系プロジェクトの主目的は実装です。つまり、主要言語のサブセットを作って満足して終わりみたいなケースがほとんどで、新規性のある機能を入れてやるぞ!みたいな意欲的なプロジェクトは相当に少ないです。
また、実装言語のバリエーションは相当豊かになりました。一昔前はどの処理系もCかC++かみたいな状況でしたが、HaskellやらF#やらC#やらRustやら沢山ありました。中でもRustは多かったです。Cライクかつ代数的データ型がある辺りが理由なんでしょうか。
実装言語のバリエーションとも関係する話ですが、対象言語と実装言語に一定の相関があるような気がします。特に構文は顕著で、キーワードとかは露骨に似ます。
また、全体的にプレーンなデータ構造をアドホックに拡張するという方向性の言語が多くなった感があります。ただ、これは自作言語に限らず、Haskellの型クラス、Rustのimpl、Rubyのopen classとrefinements、Scalaのimplicit parameter、C#の拡張メソッド、DのUFCS、Goのレシーバ、Swiftのextensionなどのメジャー言語にも見られる特徴なのでプログラミング言語設計の流行りなのかもしれません。
残念ながらGitHubにあるプログラミング言語処理系のほとんどはすぐにメンテされなくなります。継続的に1年開発が持つのはほとんどなく、みんな3ヶ月もせずに飽きてしまいます。よくRubyはRailsのおかげで流行ったと言われてますが、Rubyの開発が本格的にスタートしたのが1995年、Railsのファーストリリースが2004年なので、少なくともRails抜きで9年間開発が継続していました。その時点でDHHが(もっと言えばDave Thomasが)Rubyを見つける程度には普及していたわけですが、9年間開発が継続する言語処理系は本当に少なく、Rubyのようにワールドワイドに使われる言語を作ろうと思うと桃栗3年どころか柿8年ですらちょっと足りないほどの忍耐力と情熱が必要なわけですから大変ですね。とはいえ実際問題いかに優れた言語だろうと、ほんの9年間すらメンテされない言語を使いたくはないので、プログラミング言語を作るというのはそういうことなんでしょうね。大変ですね(他人事)。