229
107

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

言語実装Advent Calendar 2020

Day 6

最近見つけたおもしろ自作言語の紹介

Last updated at Posted at 2020-12-05

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 + 23ですが、'1 + 2'1 + 2という値になります。これは要するに{ op: add, lhs: 1, rhs: 2 }のような値を表現してると考えてください。これを実際に計算するにはeval関数を使います。eval('1 + 2)3です。
さらに、Asteroidではクォート化された式に対してパターンマッチングが可能です。つまり、let 'x + y = '1 + 2x1が、y2が束縛されます。
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内に実装を書くこともできません。常にconcretedefineの両方を求めます。なので、単純な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年間すらメンテされない言語を使いたくはないので、プログラミング言語を作るというのはそういうことなんでしょうね。大変ですね(他人事)。

229
107
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
229
107

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?