Polemy Reference Manual
Authors:
k.inaba
License:
NYSL 0.9982 (http://www.kmonos.net/nysl/)
左のサイドバーの "Package" タブをクリックすると実装のソースのドキュメントが読めます。
このファイルは、言語仕様などの、やや辞書的な説明です。
もっとざっくりとした、言語デザインの方向性の魂的なものについては、
「メタプログラミングの会」の発表スライドをご覧下さい。
あと、 やたらとマクロの章が長くなっていますが、 この部分は、
レイヤ機能を入れたら自動的にすごく自然にマクロが入るなーと思って、
おまけで実装してみた程度のものです。
あんまり重要ではないので、適当にスルーして下さいませ。
単に、適当に入れたら適当で微妙な部分が多く残ってしまったので注意書きが増えているだけで…。
言い訳ついでにもう一つ言い訳ですが、この言語は、少なくとも今のところ、
実用に使うことを考えた設計にはなっていません。どちらかというと、
Brainfuck や Unlambda や Whitespace の仲間と思ってお使い下さい。
-
Syntax
文法について。
字句解析がわりと適当なので、
変数宣言の変数名のところに、数字を変数名として使えて参照できない変数が作れたり、
予約語は予約語として解釈され得ないところでは普通に変数名として使えちゃったりして、
偶にとんでもない見かけのソースが構文解析通りますが、気にしないで適当に使って下さい。
-
文字コード
UTF-8 のみ対応です。
-
コメント
行コメントは # から改行までです。
ブロックコメントはありません。
-
BNF
ID ::= 適当に識別子っぽい文字列
LAYER ::= "@" ID
E ::=
# 変数宣言
| DECL "=" E (";"|"in") E
| DECL "(" PARAMS ")" "{" E "}" (";"|"in") E
| DECL "=" E
| DECL "(" PARAMS ")" "{" E "}"
where DECL ::= ("var"|"let"|"def"|LAYER) ID | "@" LAYER
# リテラル
| INTEGER # 非負整数
| STRING # "" でくくった文字列。\" と \\ は使える
| "{" ENTRYS "}" # テーブル
| "fun" "(" PARAMS ")" "{" E "}" # 無名関数
| "λ" "(" PARAMS ")" "{" E "}" # 無名関数
# 関数呼び出し
| E "(" ARGS")"
where ARGS ::= E "," ... "," E
PARAMS ::= (ID|LAYER)+ "," ... "," (ID|LAYER)+
ENTRYS ::= ID ":" E "," ... "," ID ":" E
# 演算子など
| "(" E ")" # ただの括弧
| "..." # これを実行するとdie
| E BINOP E # 二項演算子いろいろ
| E "." ID # テーブルのフィールドアクセス
| E ".?" ID # テーブルにフィールドがあるか否か
| E "{" ENTRYS "}" # テーブル拡張
| "if" E ("then"|":"|"then" ":") E
| "if" E ("then"|":"|"then" ":") E "else" ":"? E
# パターンマッチ
| "case" E ("when" PATTERN ":" E )*
where PATTERN ::= 式がだいたいなんでも書ける気がする
# レイヤ指定実行
| LAYER "(" E ")"
-
糖衣構文
演算子というものはありません。内部的には全て関数呼び出し構文に書き換えられています。if もです。
パターンマッチも全部 if と == と && と
. と .? を使った関数呼び出し式に書き換えられていますが、
規則の詳細を説明するのが面倒なので適当に想像して下さい。
他の書き換えはこんな感じです。
if E then E ⇒ if( E, fun(){E}, fun(){} )
if E then E else E ⇒ if( E, fun(){E}, fun(){E} )
E BINOP E ⇒ BINOP(E, E)
{ ENTRIES } ⇒ {}{ ENTRIES }
{} ⇒ {}()
E {ID:E, ...} ⇒ .=(E, ID, E) { ... }
変数宣言に色々ありますが、let と var と def は同じ扱いで、
in と ; は同じ扱いです。つまり
let x = E in E
var x = E in E
def x = E in E
let x = E ; E
var x = E ; E
def x = E ; E
以上のどれも同じ意味なので、なんとなく関数型っぽく書きたい気分の日は let in を、
手続き型っぽく書きたい気分の日は var ; を使うとよいと思います。
if then else も微妙にコロンがあったりなかったりバリエーションがありますが好みで使います。
関数を宣言するときは、fun や λ を省略できます。
以下の書き換えが行われます。
def f( ARGS ) { E }; E ⇒ def f = fun(ARGS){E}; E
他に、もっと手続き型っぽくための書き換え色々
fun () { E; E; E } ⇒ fun () { let _ = E in let _ = E in E }
fun () { var x = 100 } ⇒ fun () { var x = 100; x }
fun () { var x = 100; } ⇒ fun () { var x = 100; x }
fun () { } ⇒ fun () { "(empty function body)" }
中身が空の関数に何を返させるかは適当です。今はとりあえず適当に文字列返してます。
-
変数のスコープ規則
基本的には、let によって常識的な感じに変数のスコープがネストします。
let x=21 in let x=x+x in x # 42
一方で、"let rec" のような特別な構文はありませんが、
let f = fun(x) { if x==0 then 1 else x*f(x-1) } in f(10) # 3628800
再帰的な関数定義なども、おそらく意図されたとおりに動きます。
内部の詳細は、諸般の事情により、
マジカルで破壊的なスコープ規則になっているのですが、
同名の変数を激しく重ねて使ったりしなければ、
だいたい自然な動きをすると思います、たぶん、はい。
ひとつだけ不可思議な動きをするのは、以下のケースです。
let x = 1 in
let f = fun() {x} in
let x = 2 in
f() # 2!!
let-in を縦にチェインしたときだけ、同名変数を破壊的に上書きします
(再帰関数の定義が"うまく"いっているのはこの上書きのためです)。
なんでこんなことになっているかというと、
後で説明する「レイヤ」を使ったときに
let foo = ... in @lay foo = ... in ...
で他レイヤに重ね書きするため、のつもりです。詳しくは後で。
-
Basic Features
特に特徴的でもない部分を簡単にまとめ。
- 静的型システムはありません。
- "ほぼ" 純粋関数型言語です。変数やテーブルのフィールドの破壊的な書き換えはできません。
ただし、組み込み関数(print)と、変数のスコープ規則のマジカルな片隅に副作用があります。
静的型システムがないのは意図的ですが、破壊的代入がないのは、単に実装がめんどかっただけなので、
今後何か増えるかもしれません。増えないかもしれません。
-
データ型
以下のデータ型があります。
- 整数: 0, 123, 456666666666666666666666666666666666666789, ...
- 文字列: "hello, world!", ...
- 関数: fun(x){x+1}
- テーブル: {car: 1, cdr: {car: 2, cdr: {}}}
- ボトム: (特殊なケースで作られます。「レイヤ」の説明参照のこと。)
関数はいわゆる「クロージャ」です。静的スコープで外側の環境にアクセスできます。
テーブルはいわゆるプロトタイプチェーンを持っていて、
自分にないフィールドの場合は親に問い合わせが行く感じになっていますが、
フィールドの書き換えがないので、これは特に意味ないかもしれない…。
また、リストを扱うために、いわゆる「cons リスト」を使います。
空リストを {}、1個以上要素があるものを {car: 先頭要素, cdr: 二番目以降のリスト}
という形で。この形でリストを扱わなければならないという決まりはありませんが、
この形は特別扱いされて print で綺麗に出力されたりします。
-
パターンマッチ
適当に実装されたパターンマッチがあります。
リストの 2n 番目と 2n+1 番目を足して長さを半分にする関数:
def adjSum(lst)
{
case lst
when {car:x, cdr:{car: y, cdr:z}}: {car: x+y, cdr: adjSum(z)}
when {car:x, cdr:{}}: lst
when {}: {}
}
動かすときには、処理系がそれっぽい if-then-else に展開しています。
when を上から試していって、最初にマッチしたところを実行します。
どれにもマッチしないとエラーでプログラム終了します。
PAT ::= "_" # ワイルドカード
| ID # 変数パターン
| "{" ID ":" PAT "," ... "," ID : PAT "}" # テーブルパターン
| E # 値パターン
変数パターンは常にマッチして、値をその変数に束縛します。
ワイルドカードも常にマッチしますが、変数束縛しません。
値パターンは、任意の式が書けます。その式を評価した結果と == ならマッチします。
外で束縛された変数を値パターンとして配置、は直接はできないので
var x = 123;
case foo
when {val: x+0}: ... # これは {val:123} と同じ
when {val: x}: ... # これは任意の foo.?val なら常にマッチ
適当にちょっと複雑な式にしてやるとよいかも(裏技)。
テーブルパターンは、書かれたキーが全てあればマッチします。
{a: _} は、.a を持ってさえいればマッチするので、
{a: 123, b: 456} なんかにもマッチします。
なので、リストに対するパターンを書くときには、car/cdr の場合を先に書かないと
when {} を上に書くと全部マッチしちゃいます。注意。
-
Layers
この言語の唯一の特徴的な部分は、「レイヤ」機能です。
ひとつのコードに複数の「意味」を持たせるのが、レイヤ機能の目的です。
-
概要
普通に Polemy のコードを動かすと、そのコードは「@value レイヤ」で動作します。
インタプリタで実験。
$ bin/polemy
Welcome to Polemy 0.1.0
>> 1 + 2
3
この、普通に、数字の 1 は数字の 1 として、2 は 2 として、足し算は足し算として実行するのが、
「@value レイヤ」です。
レイヤを明示的に指定するには、レイヤ名( ... ) という構文を使います。
レイヤ指定式 と読んでいます。
つまり、さっきのコードは以下のようにも書けます。
>> @value( 1 + 2 )
3
他のレイヤで動かしてみましょう。適当に。「@hoge レイヤ」で。
>> @hoge( 3 )
polemy.failure.RuntimeException@polemy\eval.d(138):
[:4:8] lift function for @hoge is not registered
エラーになりました。Polemy のインタプリタは、起動時には、@value
レイヤでのコードの意味しか知りません。@hoge レイヤでは 3
というのがどんな意味なのか、わかりません!というエラーが出ています。
これを教えてあげるためには、@hoge レイヤの リフト関数 というものを定義します。
>> @@hoge = fun(x){ x*2 }
(function:1bdc5c0:1ba8580)
@hoge レイヤでは、1 というコードの意味は 2、
2 というコードの意味は 4、…、という、全部「2倍した意味」を持っていることにします。
「@ レイヤ名 = ...」 という構文を使います。
ここには、「@value レイヤでの値 x は @hoge レイヤではどういう意味になるか?」
を計算して返す関数を登録します。
これで、Polemy にも、@hoge レイヤの意味がわかるようになりました。
>> @hoge( 3 )
6
では、1+2 を @hoge レイヤで動かしてみましょう。
>> @hoge( 1 + 2 )
polemy.failure.RuntimeException@polemy\eval.d(466):
[:3:7] only @value layer can call native function: +
[:3:7] +
まだエラーですね。これは要するに "+" の意味がわからない、と言っています。
レイヤ指定変数定義式 で、"+" の意味を教えてあげます。
>> @hoge + = fun(x, y) {x}
(function:182eca0:18435e0)
>> @hoge( 3 + 4 )
6
できました。
他の組み込み関数の意味も決めてみましょう。この @hoge レイヤでは、
引き算のつもりで書いたコードが、掛け算になってしまうのだ!
>> @hoge - = fun(x, y) {x * y}
(function:1b4c6a0:1b4fbe0)
>> @hoge( 5 - 6 )
polemy.failure.RuntimeException@polemy\eval.d(469):
[:3:24] only @value layer can call native function: *
[:3:24] *
[:4:8] -
5、の意味は 10 で 6 の意味は 12 なので、10 - 12 と見せかけて掛け算して 120 が返るのだ!
と思いきや、エラーになってしまいました。なぜでしょう。それは、この "-" の定義、
fun(x, y) {x * y} 自体が、@hoge レイヤで実行されるからです。
掛け算はまだ定義していません。
ここは、「普通の」意味の掛け算を使いたいのです。
この部分については、@value レイヤで計算して欲しい。
そんなときは、レイヤ指定式を使います。
>> @hoge - = fun(x, y) {@value(@hoge(x) * @hoge(y))}
(function:1b086c0:1b4fbe0)
>> @hoge( 5 - 6 )
120
できました。掛け算は、@value レイヤの意味で実行します。
各変数は、@hoge レイヤで計算された意味を使います、という意味になります。
-
関数の自動リフト
続きです。ちょっと関数を定義してみました。
>> def twoMinus(x,y,z) { x - y - z }
(function:1b26420:1b4fbe0)
>> twoMinus(1,2,3)
-4
@value レイヤで実行すると、当然、1 から 2 と 3 を引いて、-4 です。
>> @hoge( twoMinus(1,2,3) )
48
@hoge レイヤだと、2 と 4 と 6 を掛け算するので、結果は 48 です。
1, 2, 3 のような値と、+ や - のような組み込み関数については、
「@hoge レイヤでの意味」をレイヤを定義する人が決めてやる必要があります。
でも、それさえ決めれば、あとはプログラム中で自分で定義した関数はすべて、
Polemy 側で自動的にそのレイヤでの意味で実行できるようになります。
レイヤ指定変数定義を使って、変数の意味をそのレイヤでだけ上書きして、
違う意味を与えてやっても構いません。
>> def twoMinus(x,y,z) { x - y - z } # @value レイヤでの定義
>> @hoge twoMinus(x,y,z) { 21 } # @hoge レイヤでの定義
>> twoMinus(1,2,3)
-4
>> @hoge( twoMinus(1,2,3) )
42
こんな感じで。
-
レイヤ指定引数
ここまでのサンプルでは、コードを書いた人が、レイヤ指定式で明示的にレイヤを切り替えていました。
レイヤ指定引数 を使うと、ライブラリ関数などを書くときに、
「この関数の第2引数は @hoge レイヤで計算して欲しい」
といった指定ができます。
>> def f(x, y @hoge) { x + @hoge(y) }
>> f(1, 2)
5
f の第2引数は、必ず @hoge レイヤで解釈されます。
>> def ff(x, y @hoge @value) { x + @hoge(y) + @value(y) }
>> ff(1, 2)
7
@hoge と @value の両方のレイヤで解釈して欲しい、という欲張りな人は、
レイヤ指定を複数並べて下さい。
なにもレイヤ指定がないと、ニュートラルレイヤ指定 と呼ばれ、
その関数の呼び出し側が解釈されていたレイヤと同じところにセットされます。
let, var, def による変数定義も同じで、
@hoge x = ... とレイヤを明示するとそのレイヤでの変数の意味が定義されますが、
let x = ... とレイヤ指定しないで書くと、現在解釈中のレイヤに定義、という動作をします。
-
ボトムと自動メモ化
パターンマッチ失敗時と、"..." という式を実行したときと、再帰が無限に止まらなくなったとき、
には、Polemy のコードは実行時エラーで終了します……@value レイヤならば。
ユーザー定義レイヤでは、このような時にも実行時エラーにならず、
「ボトム」という特別な値がリフト関数に渡されます。
(ボトムのリフトに失敗した時は、この再帰停止処理は行われません。無限ループします。)
組み込みの _isbot 関数で、ボトムかどうか判定できます。
「再帰が無限に止まらなくなったとき」は、
ある引数で呼び出された関数が、return するよりも前にまた同じ引数で呼び出されたら、
ループしていると見なすことで判定しています。
これを判定する実装の副作用として、ユーザー定義のレイヤでは、関数は全てメモ化されています。
つまり、ある関数が2回同じ引数同じ環境で呼び出されたら、1回目の答えをキャッシュしておいて、
2回目は計算をせずに即座にキャッシュをひいて答えを返します。
-
まとめ
まとめると、以下の機能があります。
- @@layer = fun(x) { ... } in ... で、
@value レイヤの値に別のレイヤでの意味を与えるリフト関数を定義
- @layer x = ... in ... で、そのレイヤでのその変数の意味を定義
- どちらも let/var/def 式の特殊形なので、@@layer(x) { ... } in ... などの略記も可。
- 式の途中で @layer( ... ) と書くと、レイヤを明示的に切り替えられる
- 関数の仮引数に fun(x @layer){ ... } とレイヤを指定すると、
対応する実引数はそのレイヤで解釈される。
-
例
具体的な「値」のかわりに、その「メタ情報」を取り出して、それが処理によってどう変化するか、
といった情報を解析するのを主な用途として、この機能を作ってみました。
プログラムでよく使われる代表的なメタ情報は、「型」です。
サンプルとしては、sample/type.pmy をご覧下さい。以下、簡単な概略。
@@type = fun(x) {
if( _isint(x) ) then "int"
else if( _isstr(x) ) then "str"
else if( _isbot(x) ) then "runtime error"
else "type error"
};
>> @type( 1 )
int
>> @type( 2 )
int
>> @type( "foo" )
str
こんな風に、値をメタ情報へ抽象化するのが、リフト関数です。
型に抽象化したレイヤでの、組み込み関数の意味を考えましょう。
"+" は、"int" と "int" を足したら "int" を返す関数です。
それ以外なら"型エラー"を返します。そういう関数です。
var int_int_int = fun (x, y) {@value(
var tx = @type(x);
var ty = @type(y);
if tx=="runtime error" then ty
else if ty=="runtime error" then tx
else if tx=="int" && ty=="int" then "int"
else "type error"
)};
@type + = int_int_int;
@type - = int_int_int;
@type < = int_int_int;
>> @type( 1 + 2 )
int
>> @type( 1 + "foo" )
type error
「実行時エラーについては、それが起きなければ返すはずの型」を計算するという定義に、
ここではしています。さらに(ちょっと手抜きで int 以外を考えていない)if の型定義を考えると、
こんな雰囲気。
@type if (c, t, e) {@value(
if( @type(c)=="int" || @type(c)=="runtime error" ) then
@type( int_int_int(t(), e()) )
else
"type error"
)};
関数が自動リフトされるので、フィボナッチ関数の型を調べることができます。
>> def fib(x) { if x<2 then 1 else fib(x-1)+fib(x-2) };
>> @type( fib(100000000000000) )
int
>> def gib(x) { if x<2 then 1 else gib(x-1)+gib(x-"str") };
>> @type( gib(100000000000000) )
type error
この定義で fib(100000000000000) を @value レイヤで普通に計算して、
結果の型を見る、というのでは時間がいくらあっても足りません。
いったん @type のメタ情報の世界に移ってから計算できるのが、レイヤ機能の肝です。
正確には、この定義で @type レイヤに移ると fib("int") を無限に呼び出し続けて止まらなくなるのですが、
そこは、自動メモ化による再帰検出でボトム値を返す機能によって、うまく止まっています。
それでも上手く型計算ができない(あるいはすごく遅くなる)ような複雑な関数があるかもしれません。
仕方がないので、型情報をアノテーションとしてつけてあげることも可能です。
@type f = int_int_int;
def f(x,y) { ...とても型を計算できないくらい複雑な定義... };
これが、レイヤ指定変数定義の典型的な使い道です。
-
Macro Layers
Polemy 言語組み込みのレイヤは @value と @macro の二つです。
(内部的にはもういくつかありますが、ユーザから直接は使えません。)
@value は、「普通に」普通のセマンティクスでプログラムを実行するレイヤでした。
@macro は、実は、@value よりも前に実行されるレイヤで、
「プログラムを実行するとその構文木を返す」というセマンティクスで動きます。
動きとしてはこうです。
- 関数呼び出し時(とトップレベル環境の実行開始時)に、
まず、
@macro レイヤでコードを実行。
- 返ってきた構文木を、その関数を呼び出したときのレイヤで実行。
@macro レイヤも所詮ただのレイヤですので、
上で説明した方法で @macro レイヤに関数などを登録しておくことで、
構文木の生成をいじることが可能です。まさにマクロ。
-
概要
samples/macro.pmy にいくつか使い方サンプルが置いてありますので、詳しくはそちらをどうぞ。
>> @macro( twice(print("Hello")) )
{
pos: {lineno:1, column:9, filename:},
args: [ { pos: {lineno:1, column:15, filename:},
args: [{pos:{lineno:1, column:21, filename:},
is:Str,
data:Hello}],
is: App,
fun: {pos:{lineno:1, column:15, filename:}, is:Var, name:print}}
],
is: App,
fun: {pos:{lineno:1, column:9, filename:}, is:Var, name:twice}
}
詳細は気にしなくて構いませんが、とにかく、@macro レイヤでは、
基本的には、コードを実行するとそのコードの構文木がでてきます。
この挙動は @macro レイヤの変数をセットすることで、カスタマイズできます。
>> @macro twice(x) { x; x } in twice(print("Hello"))
Hello
Hello
Hello
(3回出力されてますが、3個目は print(x) の返値は x なので、
それがREPLによって印字されているだけです。)
@macro レイヤで in 以降を実行すると、print("Hello") という式を表す構文木が作られ、
それが twice 関数に渡されます。twice の中身も @macro レイヤで実行されるので、
構文木を作ろうとしますが、変数 x には @macro レイヤで値が入っているので、
その値を読み取って構文木を作成します。
結果として、2回 print("Hello") する構文木が作られて、
その後で、それが @value レイヤで実行されています。
本当にベタに構文木を作るだけなので、変数名の衝突などなどは気にしません。「衛生的でない」マクロです。
@macro LetItBe(x, y) { var it = x; y }; # y の中で変数 it が使える
print( LetItBe("myself", "when I find " ~ it ~ " in times of trouble") );
変数名に気をつけるには、組み込み関数 gensym() を使って頑張って下さい。
-
レイヤ切り替え
他のレイヤ同様、@macro レイヤを実行中に @layer( ... ) 構文を使うことで、
別のレイヤでコードを動かすこともできます。よく使う例は、@value
レイヤに移ることで構文木を普通に計算して色々プログラム的にいじる用途です。
@macro reverseArgs(e) {@value(
def rev(xs, acc) {
case xs when {car:x, cdr:xs}: rev(xs, {car:x, cdr:acc}) when {}: acc
};
case @macro(e)
when {is:"App", fun:f, args:as}: {is:"App", fun:f, args:rev(as,{})}
when e: e
)};
print( reverseArgs(1-2) ); # 2-1 == 1
reverseArgs は、関数呼び出しの構文木の、引数の順番を逆転する関数です。
@macro(e) によってマクロレイヤにセットされている構文木引数を取り出し、
それを @value レイヤによる普通の計算プログラムで操作しています。
@macro(...) はいわゆる「準クオート (quasiquote)」、
@value(...) は「逆クオート (unquote)」にちょっと近いかもしれません。
@layer(...) だけでなく、関数のレイヤ指定引数なども同様に使うことができるので、
一部の引数は @macro、一部の引数は @value レイヤで受け取る関数を書くなど、
さらに色々面白いことが可能です。
-
構文木の構造
構文木がどのようなテーブルで渡されてくるかについては、ソースドキュメントの
polemy.ast
のページをご覧下さい。例えば変数名を表す Var クラスには、
継承の分も合わせて
LexPosition pos;
と string name; の2つのメンバがあるので
{ is: "Var",
pos: {filename:"foo.pmy", lineno:123, column:45},
name: "x" }
こんな感じのテーブルになります。
クラス名が is フィールドに、メンバ変数はそのままの名前で入ります。
配列メンバは cons リストになって入ってきます。
自分で構文木を作る時は、pos フィールドだけは省略しても構いません。
-
微妙なところ1
ここまで、@macro が本当にただの1レイヤと説明してきましたが、
実はちょっとトリックが潜んでいます。
>> @macro twice(x) {x; x} in twice(@value(print("Hello")))
Hello
Hello
Hello
先ほどの例に @value を増やしたものですが、これでもやはり、Hello
が2回 print されるようになります。これは本来はおかしな話で、print("Hello")
は @value レイヤで実行されて値に落ちるはずなので、1回しか print されないはず。
実は、Polemy の中では、@macro レイヤと (rawmacro)
レイヤという二つの異なるマクロ用レイヤが動いています。
- (rawmacro) も @macro も、コードを動かすとその構文木を返す意味論。
- ただし、(rawmacro) も @macro も、
@macro レイヤに値のセットされた変数をみつけたときは、
その変数という構文木を作るのではなく、変数の内容を展開。
- また @macro は、
レイヤ指定式を見ると実行レイヤを切り替て、構文木生成モードをやめてしまう。
- (rawmacro) は、
レイヤ指定式を見ても実行レイヤを切り替えないで構文木にする。
ユーザーから直接 (rawmacro) は呼べませんが、
「関数やトップレベル実行開始前のマクロ処理は (rawmacro) で実行開始」
「@macro レイヤ以外で呼び出した関数の仮引数に @macro がついていたら、
その実引数は (rawmacro) で実行」
という2つのタイミングで (rawmacro) が動き出します。
(rawmacro) が @macro レイヤから変数を見つけてマクロし始める時に、
そこで @macro に動作が移ります。
こうなっているのは、全部がレイヤ指定式に反応する @macro の動作だと、
レイヤを使ったプログラムが全て @value 実行時ではなく、
マクロ展開の時点で動き始めてしまって、おかしなことになるためです。
色々考えた結果、とりあえずこの中途半端な混合が具合がよいのではないかということになりました。
-
微妙なところ2
「関数実行開始時に、まずマクロレイヤを実行」と書きましたが、この時、関数内関数まで辿りにいくので、
何重にもネストした関数を使っていると、内側の関数は、何重にもマクロ展開が走ってしまいます。
これはなにかおかしい気がしますね。Scheme などはどうなっているのか調べないと…。
-
微妙なところ3
これはエラーになります。
>> let _ = (@macro twice(x) {x;x} in twice(print("Hello")))
polemy.failure.RuntimeException@polemy\value.d(109):
[:2:35] 'twice' is not set in @value layer
どういうことかというと、@macro で定義したマクロはいつから使えるようになるかという話で、
この @macro twice(x) {x;x} in ... の部分は @value レイヤの式なので、
まずこの式全体のマクロ展開が終わったあとにしか実行されないのです。twice
がマクロと見なされはじめるのは、@macro 実行が終わった後。
なので、
例えば twice(print("Hello")) の部分を無名関数にラップしてやれば、
マクロ展開を遅らせられて、 ちゃんと実行ができます。
これだと余りにも不便なので、関数のトップレベルの変数宣言式の列についてだけは、
@macro と @value の評価を交互にインターリーブするようにしました。
「関数やREPLのトップレベルの最初に宣言したマクロだけは、その関数内で即座に使える」わけです。
これも Scheme の let-syntax などなどの動きを調べて勉強しないと…。
-
Built-in Primitives
組み込み関数・変数の一覧。
-
テーブル操作
-
{} | () | 空のテーブルを作る |
. | (t, s) | テーブル t の名前 s のフィールドの値を取得。なければ undefined |
.? | (t, s) | テーブル t に名前 s のフィールドがあれば 1、なければ 0 |
.= | (t, s, v) | テーブル t を親に持ち、名前 s のフィールドに v が入ったテーブルを作る |
-
制御フロー
-
if | (n, ft, fe) | n が非 0 なら ft()、0 なら fe() を実行 |
-
演算
-
+ | (n, m) | 整数 n と整数 m を足して返す |
- | (n, m) | 整数の引き算 |
* | (n, m) | 整数の掛け算 |
/ | (n, m) | 整数の割り算 |
% | (n, m) | 整数の剰余 |
&& | (n, m) | 整数 n と m が両方非 0 なら 1、それ以外では 0 |
|| | (n, m) | 整数 n と m がどちらか非 0 なら 1、それ以外では 0 |
~ | (a, b) | a と b を文字列化して結合 |
< | (a, b) | a と b を比較 |
<= | (a, b) | a と b を比較 |
> | (a, b) | a と b を比較 |
>= | (a, b) | a と b を比較 |
== | (a, b) | a と b を比較 |
!= | (a, b) | a と b を比較 |
注意点として、作者の趣味の問題で、&& と || は short-circuit 評価をしません。
整数演算の種類が少ないのは、D 言語の std.bigint がビット演算などをサポートしてないためです。
文字列が結合しかできないのは、単に手抜きです。
-
外部とのやりとり
-
print | (a) | a を文字列化標準出力に改行付きで表示して、a を返す |
argv | | スクリプトに渡された引数文字列のconsリスト |
gensym | () | エセgensym。変数名として他とかぶらなそうな文字列を返します |
rand | (n) | 0 以上 n 未満の自然数を31bit以内でランダムに生成します |
-
データ型判定
-
_isint | (a) | a が整数なら 1、でなければ 0 |
_isstr | (a) | a が文字列なら 1、でなければ 0 |
_isfun | (a) | a が関数なら 1、でなければ 0 |
_istbl | (a) | a がテーブルなら 1、でなければ 0 |
_isbot | (a) | a が未定義値なら 1、でなければ 0 |
|