プログラミング言語Amberの紹介(その1)

3年ぶりに更新します。この3年間にも(途中1年以上別の事をやっていましたが)自作の言語とその処理系の開発は続けていました。そろそろ紹介記事を書き始めようと思います。ホームページは既にありますが、こちらは昨年の夏から更新していないので内容が古くなっています。本記事を元にして後で更新します。

言語の名前はAmberと言います。Amberとは日本語で琥珀という意味です。実は同名の言語処理系がいくつか存在するのですが(larsivi / amber / wiki / Home — Bitbucket, GitHub - eknkc/amber: Amber is an elegant templating engine for Go Programming Language, inspired from HAML and Jade, The Amber Programming Languageなど)、この名前には愛着がありますし、ある先生につけて頂いた名前であるということもあるので私も使用させて頂きます。

Amberは動的型のスクリプト言語で、以下の様な特徴を持ちます。

  1. 同図象性
  2. データ表現は関数子 + 引数
  3. プロトタイプベースのオブジェクトシステム
  4. 関数は全て部分関数であり関数同士は合併可能

同図象性とはプログラムとコードが同じ表現を持つという性質でLisp系言語に見られるものです。但し、データ表現はコンスセルではなく、関数子fと幾つかの引数e1,e2,...,enからなる f{e1, e2, ..., en} という形式です。同図象性とプロトタイプベースというのはメタプログラミングを強く意識した選択です。Amberは生成的プログラミングや自己反映的プログラミングなどを非常に平易に行えるように作られています。メタプログラミングを重視する事の具体的な目的はまた後で説明します。Amberの関数は引数のパターン及びガード節によってその定義域を限定される事で部分関数となり、さらに部分同士を合併する事によってあらたな関数を作る事が出来ます。

バグの報告やアイデア・ご意見をいただけますと大変ありがたいです。その場合は[twitter:@9_ties]にお願いします。Amberの仕様はまだ収束していないので細かな変更が今後も続きます。この文書は最新の仕様に合わせるよう頑張りますが、間違っていたらすみません。

ビルド・インストール

Amberのインタプリタamberは以下の手順でビルド・インストールできます。ビルドにはLinux環境とgit,make,binutilsが必要です。デフォルトでは/usr/lib/amber/以下にライブラリ群が、/usr/bin/以下にamberというスクリプト1つがインストールされます。コンフィギュレーションファイルはまだ用意していないので、お手数ですが必要ならばMakefile他を書き直して下さい。インストール先は/usr/lib/またはLD_LIBRARY_PATHに設定されたディレクトリにしてください。

% git clone https://github.com/nineties/amber.git
% cd amber
% make
% sudo make install

ビルドは以下の手順でブートストラッピングにより行われます。

  1. アセンブリ言語で書かれたrlcという簡単な手続き型言語のコンパイラをGNU Assemblerでコンパイル。
  2. rlcでrlciという簡単なLispインタプリタをコンパイル。
  3. rlciでrlvmというamberの仮想マシンのアセンブリコードを生成し、GNU Assemblerでコンパイル。
  4. rlciでrlvmのバイトコード用のリンカ(これ自体もrlvm上で動く)をコンパイル。
  5. rlciでrlvmのバイトコード用の逆アセンブラ(これ自体もrlvm上で動く)をコンパイル。
  6. rlciでamberのソースコードをコンパイルし、rlvm用のオブジェクトファイル(*.rlo)を生成。
  7. 上で作成したリンカを使って*.rloをリンク。

Hello World

% amber

でamberのシェルが起動します。(注:まだreadlineやhistoryを実装していませんので行編集が出来ません。そのうち実装します。)
もしくはファイルにプログラムを書いて

% amber hoge.ab

と実行します。Amberスクリプトの拡張子は.abです。

まずはHello Worldです。

amber:1> puts("Hello Amber!")
Hello Amber!
=> nil

シェルを終了させる為にはexitとタイプします。

他にも幾つか見てみましょう。amberシェルでは直前の計算結果を$で参照する事が出来ます。またn番目のプロンプト(amber:n> ...)の結果を$nで取り出せます。例えば以下の2 * $では 2×3を、$2 / $3では3割る6を計算しています。

amber:2> 1 + 2
=> 3
amber:3> 2 * $
=> 6
amber:4> $2 / $3
=> 0.5

また、整数は桁があふれると自動的に多倍長整数に変換されます。

amber:5> 7 ^ 160
=> 1643184774938171857917000410556544806341837419599523497069764671233207565562287891877564323818254449486910838997871467298047369612896001

基本的な数学関数は実装してありますので、とりあえず関数電卓としてそこそこ使えると思います。

amber:6> import math
=> Module{math}
amber:7> math::sin(1)
=> 0.8414709848078965

モジュール変数は :: という記号を使って参照します。importのオプションで指定して現在の名前空間に持ってくる事も出来ます。例えば以下の様にするとmath::なしでsqrtを呼べます。

amber:8> import math (sqrt)
=> Module{math}
amber:9> sqrt(2)
=> 1.414213562373095

他にもある特定の変数以外を持ってきたり、全て持ってくるといった事が出来ます。

amber:10> import math hiding (tan, cos)
=> Module{math}
amber:11> log(2)
=> 0.6931471805599454
amber:12> cos(1)
Exception: UndefinedVariable{cos}
amber:12> import math (*)
=> Module{math}
amber:13> cos(1)
=> 0.5403023058681398

シンボルは変数名などの各種「名称」を表すデータ型です。Amberの識別子([A-Za-z_][A-Za-z0-9_]*[!?]?という文法)に対応するシンボルはシングルクオートで以下の様に記述すると作成出来ます。

amber:14> 'this_is_a_symbol
=> this_is_a_symbol

一般の文字列についてはto_symメソッドでシンボル化できます。

amber:15> "this is a symbol".to_sym()
=> this is a symbol

シンボルは表に登録され、同じ文字列に対応するシンボルは常にメモリ上で同一のデータである事が保証されます。同じ値か否かは演算子 == で、データの実体が同一か否かは関数identical?で調べられます。

amber:16> "abc" == "abc"
=> true
amber:17> identical?("abc", "abc")
=> false
amber:18> 'abc == 'abc
=> true
amber:19> identical?('abc, 'abc)
=> true

その他、原始的なデータ型には真偽値true,falseと意味の無い値である事を表すnilがあります。

派生型の例としてコンテナを紹介します。良く使うコンテナ型はタプル・リスト・配列・テーブル(ハッシュ表)です。演算子[ ]で各要素にアクセス出来ます。その他コンテナの特徴に応じたメソッドが用意されています。

amber:20> (1,2,3)
=> (1, 2, 3)
amber:21> [1,2,3,4,5]
=> [1, 2, 3, 4, 5]
amber:22> Array{1, 2, "foo", "bar"}
=> Array{1, 2, "foo", "bar"}
amber:23> Table{("one", 1), ("two", 2), ("three", 3)}
=> Table{("three", 3), ("two", 2), ("one", 1)}
amber:24> $["two"]
=> 2

変数を定義する場合には := という記号を用います。多くのスクリプト言語ではいきなり変数に代入が出来ますが、Amberでは定義が必要です。

amber:25> x := 2
=> 2
amber:26> x + 1
=> 3
amber:27> x = 3
=> 3
amber:28> x
=> 3
amber:29> y = 1
Exception: UndefinedVariable{y}

制御構造はif文, while文, for文, try-catch文など手続き型言語の標準的なものが実装されています。複数行のコードブロックは { ... } で囲みます。

amber:29> if [1,5,3,4].include?(3) {
amber:29~       puts("found")
amber:29~ } else {
amber:29~       puts("not found")
amber:29~ }
found
=> nil
amber:30> i := 0
=> 0
amber:31> while (i < 10)
amber:31~       i += 1
=> nil
amber:32> i
=> 10
amber:33> for i in 1..10
amber:33~       puts i
1
2
3
4
5
6
7
8
9
10
=> nil
amber:34> for v in [1,2,3,4]
amber:34~       puts v
1
2
3
4
=> nil

あとはshift-resetベースの限定継続を実験的に実装しています。Amberのアプリケーションでは対話型セッションを積極的に使っていこうと思っているので継続はあった方が良いだろうという考えです。ただ、これを実装する可能性を考えずにガベージコレクタを実装してしまったので、大分実装が面倒な事になってしまいました。実装にあたっては以下を参考にさせて頂きました。
益子萌,浅井健一(2009)「MinCamlコンパイラにおけるshift=resetの実装」PPL09

amber:35> cont := nil
=> nil
amber:36> reset () -> {
amber:36~       puts("foo")
amber:36~       shift k -> { cont = k }
amber:36~       puts("bar")
amber:36~       shift k -> { cont = k }
amber:36~       puts("baz")
amber:36~ }
foo
=> <#Function>
amber:37> cont(nil)
bar
=> <#Function>
amber:38> cont(nil)
baz
=> nil

データの表現

Amberのデータは数値型や文字列型などの原始的オブジェクト(アトム)と、シンボルfと幾つかの引数e1,e2,...,enからなるf{e1, e2, ..., en}という形式の派生オブジェクトからなります。Amberではfをオブジェクトのヘッド、nをアリティと呼んでいます。(アリティはヘッドの属性ではなくてオブジェクトの属性です。通常の用法と異なるので奇妙かもしれませんが、他に良い名前が思い浮かびませんでした。)
fullformという関数で任意のオブジェクトをこの形式で文字列化する事ができます。また、.head, .arity, .argumentsでそれぞれを取り出す事が出来ます。(fullformという呼称はMathematicaから頂いています。)

amber:39> obj := (1,2,[3,4])
=> (1, 2, [3, 4])
amber:40> fullform(obj)
=> "Tuple{1, 2, List{3, 4}}"
amber:41> obj.head
=> Tuple
amber:42> obj.arity
=> 3
amber:43> obj.arguments
=> [1, 2, [3, 4]]

もちろん、fullformを直に入力しても良いです。

amber:44> List{1, List{2, 3}, List{4, 5}}
=> [1, [2, 3], [4, 5]]

アトムは派生オブジェクトではないのでfullformはありませんが、これらにもヘッドと呼ばれるシンボルが紐付けられておりアリティが0のオブジェクトとして派生オブジェクトと同様に取り扱う事が出来ます。

amber:45> "abc".head
=> String
amber:46> "abc".arity
=> 0
amber:47> "abc".arguments
=> []
amber:48> fullform("abc")
=> "\"abc\""

同図象性

Amberのプログラムそれ自体をAmberのデータとして取り扱う事が出来ます。Lispと同様にクオート(記号')を利用すると評価器の評価を止めて構文木を取り出す事が出来ます。出力は整形されますが、fullformを使えば通常のデータと同じ構造である事が判ります。また、関数evalfullによってそれを評価する事が出来ます。

amber:49> '{
amber:49~       s := 0
amber:49~       for i in 1..100
amber:49~               s += i^2
amber:49~       s
amber:49~ }
=> {
       s := 0
       for i in 1..100 {
           s += i^2
       }
       s
   }
amber:50> fullform($)
=> "Block{Define{s, 0}, For{i, Range{1, 100}, Block{AddAssign{s, Pow{i, 2}}}}, s}"
amber:51> evalfull($49)
=> 338350

準クオート(記号`)とアンクオート(記号!)も用意されていて、準クオートの中ではアンクオートされた部分だけ評価が行われます。

amber:52> `(1 + !(2 + 3))
=> 1 + 5

関数

関数も記号 := を用いて定義します。

amber:53> f(x) := x + 1
=> <#Function>
amber:54> f(1)
=> 2

関数の引数部には各種パターンを指定する事が出来ます。例えば定義域を整数のみに限定した部分関数は以下の様に定義します。この関数に整数以外を渡すとMatchingFailed例外となります。

amber:55> g(x @ Int) := x + 1
=> <#Function>
amber:56> g(1)
=> 2
amber:57> g(1.5)
Exception: MatchingFailed{domain = (x @ Int), [1.5]}

引数部に何らかのフォームを書くともっと複雑なパターンマッチングが出来ます。

amber:57> g(Foo{x, y}) := (x, y)
=> <#Function>
amber:58> g('Foo{1, 2})
=> (1, 2)
amber:59> g(x + y) := (x, y)
=> <#Function>
amber:60> g('(2 + 3))
=> (2, 3)

以下Amberで使用する事が出来るパターンを列挙していきます。

DontCareパターン(記号_)

amber:61> g([_, _, x, _]) := x
=> <#Function>
amber:62> g([1,2,3,4])
=> 3

リテラルパターン(定数値)

amber:63> g(0) := "zero"
=> <#Function>
amber:64> g(0)
=> "zero"
amber:65> g('[x,y,z]) := "matched"
=> <#Function>
amber:66> g('[x,y,z])
=> "matched"

可変長パターン(記号...)

amber:67> g(Foo{_, _, ...}) := "Foo object with arity >= 2"
=> <#Function>
amber:68> g('Foo{1, 2, 3})
=> "Foo object with arity >= 2"
amber:69> g('Foo{1})
Exception: MatchingFailed{domain = (Foo{_, _, ...}) | ('[x, y, z]) | (0) | ([_, _, x, _]) | (x + y) | (Foo{x, y}) | (x @ Int), [Foo{1}]}
amber:69> g([_, _, rest...]) := rest
=> <#Function>
amber:70> g([1,2,3,4,5,6])
=> [3, 4, 5, 6]

ガード節

amber:71> g(x) when x > 0 := "positive value"
=> <#Function>
amber:72> g(1)
=> "positive value"

ORパターン

amber:73> g(_ @ Int or _ @ Float) := "a number"
=> <#Function>
amber:74> g(1)
=> "a number"
amber:75> g(1.0)
=> "a number"

あと、パターンではないのですが、キーワード引数が使えます。

amber:76> g(x = 0, y = 1) := (x, y)
=> <#Function>
amber:77> g(x = 1, y = 2)
=> (1, 2)
amber:78> g(y = 3)
=> (0, 3)

この時、gという名前で定義された全ての関数は次々に合併され、一つの関数になっています。現在gの定義域がどのようになっているかは次のようにして見ることが出来ます。

amber:79> g.domain
=> domain = (x = 0, y = 1)
          | (_ @ Int or _ @ Float)
          | (x) when x > 0
          | ([_, _, rest...])
          | (Foo{_, _, ...})
          | ('[x, y, z])
          | (0)
          | ([_, _, x, _])
          | (x + y)
          | (Foo{x, y})
          | (x @ Int)

gに渡された値はdomainの上から順番にチェックされ最初にマッチした関数の実体が呼ばれます。(実際には実行時に多少の最適化が行われて動的にマッチングを行うバイトコードが生成されています。)

上の例を見れば分かるように常に最後に定義されたものが優先されるので、一般的な場合を先に定義し特殊な場合を後に書きます。例えばフィボナッチ数を返す関数fibは以下のように書くことが出来ます。

amber:80> fib(n) := fib(n-1) + fib(n-2)
=> <#Function>
amber:81> fib(0) := 0
=> <#Function>
amber:82> fib(1) := 1
=> <#Function>
amber:83> fib(20)
=> 6765

変数定義が増えてきたので一旦初期化しましょう。

amber:84> .clear()
=> nil

無名関数は以下のようにして作る事が出来ます。

amber:85> (x, y) -> x + y
=> <#Function>
amber:86> $(2, 3)
=> 5

1引数の関数は非常に良く使うので、1引数の時だけは引数の括弧を省く事が出来ます。

amber:87> x -> x + 1
=> <#Function>
amber:88> $(1)
=> 2

Amberの関数はファーストクラスオブジェクトなので他の関数に渡す事が出来ます。

amber:89> [1,2,3,4].map(x -> x + 1)
=> [2, 3, 4, 5]

関数f,gの合併は演算子|によって行う事が出来ます。この場合左側の関数がマッチングにおいて高い優先度を持ちます。

amber:90> x @ Int -> "integer" | x @ Float -> "floating point" | x @ String -> "string"
=> <#Function>
amber:91> $.domain
=> domain = (x @ Int)
          | (x @ Float)
          | (x @ String)
amber:92> $90(1)
=> "integer"
amber:93> $90(1.0)
=> "floating point"
amber:94> $90("Hello")
=> "string"

あと、レキシカルスコープのクロージャがあります。

amber:95> make_counter() := {
amber:95~       n := 0
amber:95~       () -> { n += 1 }
amber:95~ }
=> <#Function>
amber:96> cnt1 := make_counter()
=> <#Function>
amber:97> cnt2 := make_counter()
=> <#Function>
amber:98> cnt1()
=> 1
amber:99> cnt1()
=> 2
amber:100> cnt2()
=> 1
amber:101> cnt1()
=> 3
amber:102> cnt2()
=> 2

テンプレートメタプログラミング

演算子[ ]に関数fを渡すとfにマッチする部分を書き換えるという動作をします。

amber:103> obj := 'Foo{1,2,3}
=> Foo{1, 2, 3}
amber:104> obj[ 1 -> 3 | 2 -> 5 ]
=> Foo{3, 5, 3}

1 -> 3 | 2 -> 5の部分が関数ですが、そう見えないよう意図的に文法を作っています。

これを使ってテンプレートメタプログラミングを非常に簡単に実現する事が出来ます。例えば和 f(1) + f(2) + f(3) + ... + f(n) を求める擬似コードは以下のように書けます。

amber:105> '{
amber:105~      s := 0
amber:105~      for i in 1..n
amber:105~              s += f(i)
amber:105~      s
amber:105~ }
=> {
       s := 0
       for i in 1..n {
           s += f(i)
       }
       s
   }

これを先ほどの方法で書き換えれば実行可能なコードを生成する事が出来ます。

amber:106> $[ 'f(i) -> '(1/i^2) | 'n -> 100 ]
=> {
       s := 0
       for i in 1..100 {
           s += 1 / i^2
       }
       s
   }
amber:107> evalfull($)
=> 1.634983900184892

これを元にマクロを作ってみましょう。Amberはフォームをmacroという関数で変換してから評価します。標準では以下のようなフォームに対するマクロが定義されています。

amber:108> macro.domain
=> domain = (e @ Import{mods, asname, option})
          | (DefTrait{head})
          | (DefClass{head, fields})
          | (WithSlots{obj, init})
          | (When{args, guard} -> body)
          | (Domain{args, List} -> body)
          | ((for i in a..b body))
          | ((for i in container body))
          | (ArithAssign{x[idxs...], e, op})
          | (x[idxs...] = e)
          | (x[idxs...])
          | (x |= y)
          | (x %= y)
          | (x //= y)
          | (x /= y)
          | (x ^y)
          | (x **= y)
          | (x *= y)
          | (x -= y)
          | (x ++= y)
          | (x += y)
          | (x <=> y)
          | (x != y)
          | (x == y)
          | (x >= y)
          | (x <= y)
          | (x > y)
          | (x < y)
          | (x | y)
          | (x % y)
          | (x // y)
          | (x / y)
          | (x^y)
          | (x ** y)
          | (x * y)
          | (x ++ y)
          | (x - y)
          | (x + y)
          | (-x)
          | (+x)
          | (|x|)
          | (Assign{Send{obj, f, args}, e, g})
          | (Define{Send{obj, f, args}, e, g})
          | (Assign{Apply{f, args}, e, g})
          | (Define{Apply{f, args}, e, g})
          | (Send{obj, f, args} = e)
          | ((Send{obj, f, args} := e))
          | ((Apply{f, args} := e))
          | (Apply{f, args} = e)
          | (e)

これを拡張して f(1) + f(2) + ... + f(n) を計算するマクロ sum(f, i, n) を作りましょう。"sum"の部分はsumというシンボル自体にマッチして欲しいのでリテラルパターン('sum)を使います。また、テンプレート内で使われている変数sは展開後に名前が衝突しないように、ユニークな名前に先に置き換えておきます。symbolモジュールのunique関数はシンボル表に登録されていない(従って他のシンボルと常に異なる)シンボルを生成します。

amber:109> macro( ('sum)(f, i, n) ) := {
amber:109~      template := '{
amber:109~              s := 0
amber:109~              for i in 1..n
amber:109~                      s += f(i)
amber:109~              s
amber:109~      }
amber:109~      u := symbol::unique()
amber:109~      template['s -> u]['f(i) -> f | 'i -> i | 'n -> n]
amber:109~ }
=> <#Function>

これだけで新しいフォームを使う事が出来ます。sという名前の変数を使っても正しく動作している事が分かると思います。

amber:110> sum(i^2, i, 1000)
=> 333833500
amber:111> sum(1/s, s, 10000)
=> 9.787606036044345
amber:112> sum(sum(i*j, i, j), j, 100)
=> 12920425

Lispでは準クオートを用いてテンプレートを記述する方式が使われますが、私はかねてから埋め込む方式よりも変換する方式の方が理解しやすいのではないかと考えていました。また、上の方式ならば準クオートとアンクオートが用意されていない言語に対してもテンプレートのインスタンシエーションが簡単に出来ます。この方式は標準ライブラリの実装でも使ってみましたが、コードがすっきりとし今の所は良い印象を持っています。準クオート方式と比べたデメリットについてはまだ十分に考察が出来ていません。ちなみに先ほどのマクロを準クオート方式で書くと以下の様になります。

amber:113> macro( ('sum)(f, i, n) ) := {
amber:113~      u := symbol::unique()
amber:113~      `{
amber:113~              !u := 0
amber:113~              for !i in 1..!n
amber:113~                      !u += !f
amber:113~              !u
amber:113~      }
amber:113~ }
=> <#Function>

パーサ生成

前節の方法はAmberのテンプレートでAmberのコードを生成する例でしたのでAmberの文法で全て書くことが出来ました。しかし、他の言語の構文木を作成したい場合にはAmberの文法が使えないので全てfullformで書かなければならなくなってしまいます。そこでパックラット構文解析器のジェネレータが用意されており、Amber内でパーサを作成出来るようになっています。

実はこれまで使ってきたAmberの文法自体もこのジェネレータで書かれています。インタプリタにあらかじめ組み込まれている文法はアトム及びfullformのみで、起動してから自身のパーサをコンパイルする方式になっています。(起動が遅いのは主にこの為です。いずれプリコンパイルにします。)これはlib/syntax/parse.ab内で行われており、最初はfullformのみでコードを書く→文法を定義する為の文法を定義する→Amberの文法を定義する→マクロの仕組みを作る→各種マクロを定義するという流れになっています。

以下はこのジェネレータを使ってCAST(C Abstract Syntax Tree)というC言語風の言語を定義したライブラリの一部です。基本は解析表現文法(PEG)ですが、カンマ区切りのような良く使うパターンをパースする為のdelimitedなどいくつかユーティリティが追加されています。他にも空白の扱いなどが異なるので、解説をAmberのホームページに書くつもりです。

primary_expr ::= identifier      { Var.new($0) }
               | constant
               | string          { StringL.new($0) }
               | "(" expr ")"    { $1 }

postfix_expr ::= postfix_expr "[" expr "]"
                 { Subscript.new($0, $2) }
               | postfix_expr "(" delimited(assign_expr, ",") ")"
                 { Call.new($0, $2) }
               | postfix_expr "." identifier
                 { Select.new($0, $2) }
               | postfix_expr "->" identifier
                 { Select.new(Deref.new($0), $2) }
               | postfix_expr "++"
                 { PostInc.new($0) }
               | postfix_expr "--"
                 { PostDec.new($0) }
               | primary_expr

Amberのシェルから以下のように使えます。

amber:114> import CAST
=> Module{CAST}
amber:115> template := '<CAST>{
amber:115~      struct hoge {
amber:115~              int x;
amber:115~              double y;
amber:115~      }
amber:115~      int main(int argc, char **argv) {
amber:115~              struct hoge st;
amber:115~              st.x = 0;
amber:115~              st.y = 2.0;
amber:115~              printf("%d, %f\n", st.x, st.y);
amber:115~      }
amber:115~ }
=> CASTProgram{[DefAggregates{none, StructT{hoge}, hoge, [(int, x), (double, y)]},
    DefFunc{none, int, main, [(int, argc), (PointerT{PointerT{char}}, argv)], {
        [DefVar{none, StructT{hoge}, st, nil}, Select{Var{st}, x} = IntL{0},
         Select{Var{st}, y} = FloatL{2.0},
         Call{Var{printf}, [StringL{"%d, %f\n"}, Select{Var{st}, x},
          Select{Var{st}, y}]}] }}]}

このようなテンプレートはもちろん先ほどと同様に操作する事が出来ます。上ではAmberのコード中に埋め込みましたが、CASTのコードが書かれた外部ファイルをパーズする事も出来ます。あとはファイルにコードを書き出すpretty printerを実装すればCAST -> CASTの変換器が出来上がります。今は実験的な実装ですがいずれちゃんとC言語のパーサを実装します。(PEGでは書けないのでパーサを直に書き下す形になります。) また各種最適化もライブラリ関数として実装する予定です。このライブラリはFFIの実装などで使います。Amberの次期VMの実装にもこれを使い、libc他既存のライブラリを使えるようにするつもりです。

パーサは個々の構文要素が独立した関数として生成されます。

amber:116> syntax::expr
=> <#Function>
amber:117> syntax::stmt.domain
=> domain = (parser @ Parser) when *hidden*
          | (parser @ Parser) when *hidden*
          | (_ @ Parser)

従って、関数合併を使って既存の文法を拡張する事が出来ます。

amber:118> syntax::mystmt ::= "mystatement" "{" stmt "}" { `printf("my statement %p\n", '!$2) }
=> <#Function>
amber:119> syntax::stmt = syntax::mystmt | syntax::stmt
=> <#Function>
amber:120> mystatement{ puts("Hello") }
my statement Apply{puts, ["Hello"]}
=> nil

言語の構文に何らかの拡張を行う場合、パーサ全体が一つの実体として作られている場合にはソースコードに手を入れる必要があります。上の方法ならばソースコードを書き換えずに外側から拡張を追加する事が出来ます。

オブジェクトシステム

Amberにはプロトタイプベースのオブジェクトシステムが実装されています。

  1. オブジェクトにはスロットを追加する事が出来る。
  2. parentという特別なスロットがあり、参照されたスロットが見つからなかった場合にはparentを見に行く。
  3. obj.f(...)という形で関数が呼び出された場合、f内部ではselfによってobjを参照する事が出来る。
amber:121> a := 'Foo{}
=> Foo{}
amber:122> b := 'Bar{}
=> Bar{}
amber:123> a.f() = "self=" ++ str(self)
=> <#Function>
amber:124> b.parent = a
=> Foo{}
amber:125> a.f()
=> "self=Foo{}"
amber:126> b.f()
=> "self=Bar{}"

self.x という形は頻繁に登場するのでselfを省略して.xと書けるようになっています。

amber:127> a.x = 0
=> 0
amber:128> a.g() = puts(.x)
=> <#Function>
amber:129> a.g()
0
=> nil

また, オブジェクトの生成とスロットの追加を一度に行えるように obj with { スロットの初期化 } という構文も作りました。

amber:130> obj := 'Foo{} with {
amber:130~      .x = 1
amber:130~      .y = 2
amber:130~      .f() = .x + .y
amber:130~ }
=> Foo{}
amber:131> obj
=> Foo{}
amber:132> obj.x
=> 1
amber:133> obj.y
=> 2
amber:134> obj.f()
=> 3

親と子が同名の関数を持つ場合、子の関数が親の関数を隠蔽するような実装が多いと思いますが、Amberでは親と子の関数を合併した新たな関数が子に追加されます。例えば以下のようになります。

amber:135> .clear()
=> nil
amber:136> a := 'Foo{} with {
amber:136~      .f(x) = "parent"
amber:136~ }
=> Foo{}
amber:137> b := 'Foo{} with {
amber:137~      .parent = a
amber:137~      .f(0) = "child"
amber:137~ }
=> Foo{}
amber:138> b.f(1)
=> "parent"
amber:139> b.f(0)
=> "child"
amber:140> a.f(0)
=> "parent"

合併したくない場合には、obj.f(...) := という構文を使わないでobj.fに無名関数を代入するように書けばよいです。

これを使って面白い事が出来ます。

amber:141> fib := 'Fib{} with {
amber:141~      .f(n) = .f(n-1) + .f(n-2)
amber:141~      .f(0) = 0
amber:141~      .f(1) = 1
amber:141~ }
=> Fib{}
amber:142> fib2 := 'Fib{} with {
amber:142~      .parent = fib
amber:142~      .f(0) = 1
amber:142~ }
=> Fib{}
amber:143> fib.f(20)
=> 6765
amber:144> fib2.f(20)
=> 10946

fib2ではf(0) = 1以外は親の定義を使って新しい関数を合成しています。.f(n-1) + .f(n-2) の部分で呼び出されるfの実体がselfによって動的に決まるOpen recursionという性質とAmberの関数合併のメカニズムを組合せて使っています。このように「親を一切書き換えずに機能を拡張したバージョンを新たに作る」という事を非常に手軽に実現出来ます。

Amberのモジュールは実は通常のオブジェクトであり上の仕組みが標準ライブラリで実装されています。.push()というメソッドは、現在の変数表の子供を新たに作ります。.pop()は変数表を破棄して親に戻します。これによって.push()から.pop()までの間の差分のみが破棄されます。

amber:145> x := "old version"
=> "old version"
amber:146> .push()
=> nil
amber:147> x := "new version"
=> "new version"
amber:148> .pop()
=> nil
amber:149> x
=> "old version"

以下の様にOpen recursionによって各モジュール変数は常に最新の変数表を参照します。この点がサブスコープを作るのとは異なります。

amber:150> f() := x
=> <#Function>
amber:151> .push()
=> nil
amber:152> x := "new version"
=> "new version"
amber:153> f()
=> "new version"
amber:154> .pop()
=> nil
amber:155> f()
=> "old version"
amber:156> {
amber:156~      x := "new version"
amber:156~      f()
amber:156~ }
=> "old version"

以下の例は、一時的に整数の四則演算を全て有理数の物に置換え、popによってそれを元に戻すという例です。

amber:157> 1/2
=> 0.5
amber:158> .push()
=> nil
amber:159> import algebra::rational (*)
=> Module{rational}
amber:160> 1/2 + 2/3
=> 7 / 6
amber:161> s := 0
=> 0
amber:162> for i in 1..100
amber:162~      s += 1/i
=> nil
amber:163> s
=> 14466636279520351160221518043104131447711 / 2788815009188499086581352357412492142272
amber:164> .pop()
=> nil
amber:165> 3/5
=> 0.6

このようにAmberではオブジェクトシステムをオブジェクト指向の為というよりはむしろ変数の管理に為に導入しています。一応、以下のように原始的なクラスやトレイトは標準ライブラリで用意していますがこちらは低機能です。

amber:166> class Point{x, y}
=> Class{Point}
amber:167> p := Point.new(x = 1, y = 2)
=> Point{1, 2}
amber:168> trait PairLike with {
amber:168~      .to_pair() := (self[0], self[1])
amber:168~ }
=> Trait{PairLike}
amber:169> Point.extend(PairLike)
=> Class{Point}
amber:170> p.to_pair()
=> (1, 2)

紹介その1終わり

その2ではAmberのアプリケーションとして、対話セッションにより短いテンプレートからC言語のコードを生成するという数値計算からの例を紹介したいと思います。