KMCのhatsusatoです。
この記事はKMC Advent Calendar 2021の20日目の記事です。
Json As a Programming language And object Notation[1]
構文がJSONそのものになっている純粋関数型プログラミング言語です。
{
"let": [
{
// 階乗を計算する関数factorialを定義
"function": {"factorial": 0},
"": {
"let": {"n": "factorial"},
"": {
"if": {"<": [0, "n"]},
"then": {
"let": {"n-1": {"-": ["n", 1]}},
"": {"*": ["n", {"factorial": "n-1"}]}
},
"else": 1
}
}
}
],
"": {
// 120を出力
"print": {"factorial": 5}
}
}
LispはS式でできているのがえらい[2]ので、そのS式をJSONで置き換えた言語は作れないだろうかと考えました。多分どこかで誰かが考えたことのあるアイデアだろうと思いますが、言語設計は十人十色なので、気にせず言語仕様を紹介しようと思います。
任意のJSON文字列がJapan言語として評価できます[3]が、ちゃんとした計算を行うには、Japan言語に合わせたJSONを構築してやる必要があります。
eval({k1: v1, k2: v2}) = {k1: eval(v1), k2: eval(v2)}
eval([v1, v2, v3]) = [eval(v1), eval(v2), eval(v3)]
.
は特別な意味をもつ。.
以外の文字は、JSONとして許される限り、好きなものを変数名に用いることができる。""
も有効な変数名として解釈される。.
が現れるときは、先頭から.
を1つ除いた文字列のリテラルとして解釈される。
".abc"
は長さ3の文字列abc
を意味する。".."
は長さ1の文字列.
を意味する。"."
は長さ0の空文字列を意味する。.
が含まれるときは、JavaScriptにおけるobjectのプロパティアクセスと同様に評価する。
x
の評価結果が{"a": 1, "b": 2}
のとき、x.a
はx
のキーa
に対応する値1
へと評価される。
.
の右辺は変数名ではなく字句通りに解釈される。x
がa
を含まないときはnull
へと評価される。x
がobjectでないとき、x.a
はnull
へと評価される。.
は左結合。let
マクロ"let"
と""
をキーにもつobject。
{
"let": {"x": 1},
"": "x"
}
let
マクロによるローカル変数の導入"let"
キーの値がobjectのとき、そのobjectの各キーを変数名として環境に導入した上で""
キーの値を評価する。
// 疑似コードによる評価の流れ
eval({"let": {k: v1}, "": x}, {k: v2})
= eval(x, {k: eval(v1, {k: v2})})
eval
の第2引数が環境を表すとする。k
の値としては、このlet
マクロの外側の環境{k: v2}
のもとでの評価値eval(v1, {k: v2})
を用いる。""
キーの値x
を評価し、その結果がこのlet
マクロ全体の評価結果となる。""
キーの値の中に含まれるstringが表す変数名は、このlet
マクロによって導入された環境の値を参照する。
null
へと評価される。
{ // 環境: {}
"let": {
"x": 1,
"y": 2,
"z": "x" // null
},
"": { // 環境: {"x": 1, "y": 2, "z": null}
"let": {
"x": "y", // 2
"y": { // 環境: {"x": 1, "y": 2, "z": null}
"let": {
"y": "x" // 1
},
"": [ // 環境: {"x": 1, "y": 1, "z": null}
"x", // 1
"y", // 1
"z" // null
]
}
},
"": [ // 環境: {"x": 2, "y": [1, 1, null], "z": null}
"x", // 2
"y", // [1, 1, null]
"z" // null
]
}
} // [2, [1, 1, null], null]
let
マクロによる関数定義・マクロ定義の導入"let"
キーの値がarrayのとき、array中の各要素を関数定義objectまたはマクロ定義objectとして解釈して環境に導入した上で、""
キーの値を評価する。
"function"
キーおよびマクロ定義objectの"macro"
キーの値はobjectでなくてはならず、そうでない定義は無視される。let
マクロによって導入される関数定義・マクロ定義の名前(正確には後述するシグネチャ)が環境中のものと競合するとき、既存の定義は隠蔽される。
"let"
キーの値がobjectでもarrayでもないとき、"let"
キーの値は無視してそのまま""
キーの値を評価する。
{
"let": [
{ // 関数incrementを環境に登録
"function": {"increment": null},
"": {"+": ["increment", 1]}
},
{ // マクロtwiceを環境に登録
"macro": {"twice": null},
"": ["twice", "twice"]
}
],
"": {"twice": {"increment": 1}}
}
"function"
と""
をキーにもつobject。
1
を足すincrement
関数の定義と、increment
関数への実引数として2
を渡した呼び出しである。
{
"let": [
{ // 関数定義
"function": {"increment": null},
"": {"+": ["increment", 1]}
}
],
"": { // 関数呼び出し
"increment": 2
} // 3
}
"function"
キーの値はobjectである必要があり、このobjectのキーが関数のシグネチャとなる。
""
キーの値の中では、シグネチャのキー(上の例ではincrement
)が変数名として参照でき、この変数に関数呼び出し時の実引数が束縛される。""
キーの値の中では、シグネチャのキー以外の自由な変数名は、関数定義時の環境における値を参照する。
let
マクロによって導入される束縛変数は、外の環境ではなくその場で束縛した値を参照する。""
キーの値を評価する。
// 疑似コードによる評価の流れ
eval({
"let": [
{"function": {f: null}, "": body}
],
"": {
"let": {k: v2},
"": {f: arg}
}
}, {k: v1})
= eval({
"let": {k: v2},
"": {f: arg}
}, {k: v1} with (f, body, {k: v1})) // 関数定義時の環境を記憶(レキシカルスコープ)
= eval({f: arg}, {k: v2} with (f, body, {k: v1}))
= eval({
"let": {
f: eval(arg, {k: v2} with (f, body, {k: v1})) // argは呼び出されたときの環境で評価
},
"": body
}, {k: v1} with (f, body, {k: v1})) // bodyは定義されたときの環境で評価
"macro"
と""
をキーにもつobject。
"twice"
キーの値を2つもつarrayへと展開するマクロ定義と、そのマクロを用いた1
を2つもつarrayの構成を表している。
{
"let": [
{ // マクロ定義
"macro": {"twice": null},
"": ["twice", "twice"]
}
],
"": { // マクロ展開
"twice": 1
} // [1, 1]
}
"macro"
キーの値はobjectである必要があり、このobjectのキーがマクロのシグネチャとなる。
""
キーの値の中では、シグネチャのキーが変数名として参照でき、この変数の位置にマクロに引き渡された値が埋め込まれる。""
キーの値の中では、シグネチャのキー以外の自由な変数名は、マクロ定義時の環境における値を参照する。
let
マクロによって導入される束縛変数は、外の環境ではなくその場で束縛した値を参照する。quote
マクロの挙動。""
キーの値を評価する。
// 疑似コードによる評価の流れ
eval({
"let": [
{"macro": {m: null}, "": body}
],
"": {
"let": {k: v2},
"": {m: arg}
}
}, {k: v1})
= eval({
"let": {k: v2},
"": {m: arg}
}, {k: v1} with (m, body, {k: v1})) // マクロ定義時の環境を記憶
= eval({m: arg}, {k: v2} with (m, body, {k: v1}))
= eval({
"let": {
m: eval({"quote": arg}, {k: v2} with (m, body, {k: v1}))
},
"": body
}, {k: v1} with (m, body, {k: v1}))
quote
で囲んだだけの仕様。if
マクロ"if"
, "then"
, "else"
の3つをキーにもつobject。
{
"if": ".",
"then": 1,
"else": 0
} // 0
"if"
キーの値のみを評価し、その値がfalse
, 0
(-0
), NaN
, 空文字列, null
のとき偽、それ以外のときに真とする。
"if"
キーの値の評価結果が真のとき、"then"
キーの値のみを評価し、その結果をif
マクロ全体の評価結果とする。"if"
キーの値の評価結果が偽のとき、"else"
キーの値のみを評価し、その結果をif
マクロ全体の評価結果とする。quote
マクロ"quote"
のみをキーにもつobject。
{
"quote": {"if": null, "then": 1, "else": "x"}
}
quote
マクロは引数のJSONの構造をそのままの値として扱えるようにする。.
を付加したstringとして解釈される。
{
"let": {
"q": {
"quote": {"if": null, "then": "x", "else": 0}
}
},
"": ["q.if", "q.then", "q.else"] // [null, "x", 0]
}
現在のところは未定だが、今後もう少し追加する可能性はある。
特にquote
の逆操作としてeval
マクロがあってもよい気はするが、マクロ処理の仕様そのものをもう少し煮詰めたあとで考えたい。
+
, -
, *
, /
<
, >
, <=
, >=
, ==
, !=
&&
, ||
, !
length
, at
read
, write
, print
null
を返したり無視したりしているので、そういったところで処理系が警告を出すなり例外を出すなりするようなオプションは提供するべきでしょう。知人に名前をつけてもらいました。プログラミング言語としてのJSONというニュアンスが入っていて、略語としても面白いです。 ↩︎
すなわち、プログラム中で扱われるデータ構造であるS式がプログラムのコードのデータ構造としても採用され一致しているところがえらいということ。これによって、マクロの威力が存分に発揮される。 ↩︎
本来ならwell-definedにならないようなところで無理やり値を定めているだけなので、ちょっと強弁かもしれません。 ↩︎
ざっくり言うと自分自身の言語で実装したインタプリタのこと。Japan言語はソースコードがJSONなので、Japan言語の中でコードをデータとして直接計算できる。Lispでの話はSICP(計算機プログラムの構造と解釈)を参照。 ↩︎
難解プログラミング言語(Esoteric programming language)のこと。Brainf**kなどが有名か。KMC内では一時期Pietが流行っていました。esolangコードゴルフ大会が開かれたりもしています。 ↩︎