JSONでもプログラムを書けるらしい
「JSONでもプログラムを書けるらしい」
と言ったら他人事に聞こえますが、今回JSONのフォーマットに則った形でプログラムを組み立てるプログラミング言語/インタプリタを作りました。
その名も「JSOP」です。JSON Processorの略称で、何を隠そうLISP(LIST Processor)に倣って命名しました。
本記事は「プログラミング言語作ったよ」という以上の話はないのですが、モチベーションやら簡単な言語仕様などを説明できればと思います。こんな言語も有り得るのだなと楽しんで貰えたら幸いです。
{
"command": {
"symbol": "print",
"args": "Hello, World!"
}
}
🔥 モチベーション
LISPを勉強していた時に、「データとコードが同一である」という点がLISPをユニークなものにしているという記述を目にしました。
データとプログラムがどちらもリストというデータ構造で構成されているためにそのような特徴を持っているのですが、それを知った時にリスト以外のデータ構造であるJSON(JavaScriptのオブジェクト表現)でプログラムを作れたら、LISPのような「データとコードが同一である」言語を作れるのではないかと思いJSOPへの開発に至りました。
💾 インストール方法
Homebrew Tap
brew install JunNishimura/tap/JSOP
go intall
go install github.com/JunNishimura/jsop@latest
GitHubから直接ダウンロード
GitHub releaseより実行ファイルを直接ダウンロードする。
💻 使い方
REPLは用意していないので、プログラムを任意のファイルに記載し、そのファイルパスをコマンドライン引数として渡してあげることでプログラムを実行することができます。
jsop ./test.jsop
📖 言語仕様
ファイル拡張子
ファイル拡張子としては
- .jsop
- .jsop.json
の2つを受け入れるようにしています。
共通
- 全てが式(Expression)
- JSONの構文に従う
Integer
単純に整数値を記述するだけです。
123
String
ダブルクオーテーションで囲まれた文字、記号、数字等を含む一連の列がStringです。
"Hello, world!"
Boolean
true
, false
のいずれかです。
true
Array
ブラケットで囲まれており、各要素がカンマ区切りとなっているものがArrayです。上述したIntegerやStringの他にも、下にて紹介するIfやLoopも式なので、Arrayの要素として含めることができます。
[
1,
"hoge",
true
]
Identifier
- String
- 先頭の文字が
$
を満たすものがIdentifierです。
"$hoge"
Identifierの文字列への埋め込みはカーリーブラケットを使うことで実現します。
"{$hello}, world!"
Assignment
代入に関してはset
キーを使うことで実現します。
親キー | 子キー | 説明 |
---|---|---|
set | 代入の開始合図 | |
var | 変数名 | |
val | 代入する値 |
[
{
"set": {
"var": "$x",
"val": 10
}
},
"$x"
]
Function
Function Definition
関数定義ではLambda Expressionを使用します。
親キー | 子キー | 説明 |
---|---|---|
lambda | Lambda文の開始合図 | |
params | 引数(省略可) | |
body | 関数本体 |
{
"set": {
"var": "$add",
"val": {
"lambda": {
"params": ["$x", "$y"],
"body": {
"command": {
"symbol": "+",
"args": ["$x", "$y"]
}
}
}
}
}
}
Function Call
関数呼び出しに関してはcommand
キーを使用します。
親キー | 子キー | 説明 |
---|---|---|
command | 関数呼び出しの開始合図 | |
symbol | 呼び出し関数 | |
args | 引数(引数なければ省略可) |
{
"command": {
"symbol": "+",
"args": [1, 2]
}
}
Builtin Functions
ビルトイン関数には下記のようなものがあります。
関数 | 説明 |
---|---|
+ | 足し算 |
- | 引き算 |
* | 掛け算 |
/ | 割り算 |
% | モジュロ演算 |
! | 否定 |
&& | AND演算 |
|| | OR演算 |
== | イコール |
!= | ノットイコール |
> | 大なり |
>= | 大なりイコール |
< | 小なり |
>= | 小なりイコール |
標準出力へ出力 | |
len | 配列の長さを取得 |
at | 指定した配列要素にアクセス |
If
If文ではif
キーを使用します。
親キー | 子キー | 説明 |
---|---|---|
if | If文の開始合図 | |
cond | 条件式 | |
conseq | condがtrueの時に実行される式 | |
alt | condがfalseの時に実行される式(省略可) |
{
"if": {
"cond": {
"command": {
"symbol": "==",
"args": [1, 2]
}
},
"conseq": {
"command": {
"symbol": "+",
"args": [3, 4]
}
},
"alt": {
"command": {
"symbol": "*",
"args": [5, 6]
}
}
}
}
Loop
ループ処理ではloop
キーを使用します。
親キー | 子キー | 説明 |
---|---|---|
loop | ループ処理の開始合図 | |
for | ループカウンタの識別子 | |
from | ループカウンタの初期値 | |
until | ループの終了条件(ループカウンタがこの値と同じ時にbreak) | |
do | 繰り返し処理本体 |
{
"loop": {
"for": "$i",
"from": 0,
"until": 10,
"do": {
"command": {
"symbol": "print",
"args": "$i"
}
}
}
}
Arrayの要素に対してループ処理を実行することもできます。上の例とは異なり、in
キーにArrayを指定します。
[
{
"set": {
"var": "$arr",
"val": [10, 20, 30]
}
},
{
"loop": {
"for": "$element",
"in": "$arr",
"do": {
"command": {
"symbol": "print",
"args": "$element"
}
}
}
}
]
また、break
, continue
は下記のようにキーとして挿入します。
[
{
"set": {
"var": "$sum",
"val": 0
}
},
{
"loop": {
"for": "$i",
"from": 1,
"until": 15,
"do": {
"if": {
"cond": {
"command": {
"symbol": ">",
"args": ["$i", 10]
}
},
"conseq": {
"break": {}
},
"alt": {
"if": {
"cond": {
"command": {
"symbol": "==",
"args": [
{
"command": {
"symbol": "%",
"args": ["$i", 2]
}
},
0
]
}
},
"conseq": {
"set": {
"var": "$sum",
"val": {
"command": {
"symbol": "+",
"args": ["$sum", "$i"]
}
}
}
},
"alt": {
"continue": {}
}
}
}
}
}
}
},
"$sum"
]
Retrun
returnで処理を抜け出した時はreturn
キーを使用します。
[
{
"set": {
"var": "$f",
"val": {
"lambda": {
"body": [
{
"set": {
"var": "$sum",
"val": 0
}
},
{
"loop": {
"for": "$i",
"from": 1,
"until": 11,
"do": {
"if": {
"cond": {
"command": {
"symbol": ">",
"args": ["$i", 5]
}
},
"conseq": {
"return": "$sum"
},
"alt": {
"set": {
"var": "$sum",
"val": {
"command": {
"symbol": "+",
"args": ["$sum", "$i"]
}
}
}
}
}
}
}
}
]
}
}
}
},
{
"command": {
"symbol": "$f"
}
}
]
Macro
マクロ定義には、defmacro
キーを使用します。
親キー | 子キー | 説明 |
---|---|---|
defmacro | マクロ定義の開始合図 | |
name | マクロの名前 | |
keys | キーの一覧 | |
body | マクロの本体 |
また、クォート処理にはquote
シンボルを呼び出し、文字列の先頭にバッククォートを付ける事でアンクォートすることができます。
[
{
"defmacro": {
"name": "unless",
"keys": ["cond", "conseq", "alt"],
"body": {
"command": {
"symbol": "quote",
"args": {
"if": {
"cond": {"command": {"symbol": "!", "args": ",cond"}},
"conseq": ",conseq",
"alt": ",alt"
}
}
}
}
}
},
{
"unless": {
"cond": {
"command": {
"symbol": ">",
"args": [2, 1]
}
},
"conseq": {
"command": {
"symbol": "print",
"args": "not greater"
}
},
"alt": {
"command": {
"symbol": "print",
"args": "greater"
}
}
}
}
]
関数定義もマクロを利用すればシンプルにできます。
[
{
"defmacro": {
"name": "defun",
"keys": ["name", "params", "body"],
"body": {
"command": {
"symbol": "quote",
"args": {
"set": {
"var": ",name",
"val": {
"lambda": {
"params": ",params",
"body": ",body"
}
}
}
}
}
}
}
},
{
"defun": {
"name": "$add",
"params": ["$x", "$y"],
"body": {
"command": {
"symbol": "+",
"args": ["$x", "$y"]
}
}
}
},
{
"command": {
"symbol": "$add",
"args": [1, 2]
}
}
]
Comment
コメントに関しては任意の箇所で//
キーを使うことで挿入することができます。
{
"//": "this is a function to add two values",
"set": {
"var": "$add",
"val": {
"lambda": {
"params": ["$x", "$y"],
"body": {
"command": {
"symbol": "+",
"args": ["$x", "$y"]
}
}
}
}
}
}
🤔 FizzBuzz問題
最後にJSOPでFizzBuzz問題を解いてみた例を載せておきます。
[
{
"set": {
"var": "$num",
"val": 31
}
},
{
"loop": {
"for": "$i",
"from": 1,
"until": "$num",
"do": {
"if": {
"cond": {
"command": {
"symbol": "&&",
"args": [
{
"command": {
"symbol": "==",
"args": [
{
"command": {
"symbol": "%",
"args": ["$i", 3]
}
},
0
]
}
},
{
"command": {
"symbol": "==",
"args": [
{
"command": {
"symbol": "%",
"args": ["$i", 5]
}
},
0
]
}
}
]
}
},
"conseq": {
"command": {
"symbol": "print",
"args": "{$i}: FizzBuzz"
}
},
"alt": {
"if": {
"cond": {
"command": {
"symbol": "==",
"args": [
{
"command": {
"symbol": "%",
"args": ["$i", 3]
}
},
0
]
}
},
"conseq": {
"command": {
"symbol": "print",
"args": "{$i}: Fizz"
}
},
"alt": {
"if": {
"cond": {
"command": {
"symbol": "==",
"args": [
{
"command": {
"symbol": "%",
"args": ["$i", 5]
}
},
0
]
}
},
"conseq": {
"command": {
"symbol": "print",
"args": "{$i}: Buzz"
}
},
"alt": {
"command": {
"symbol": "print",
"args": "$i"
}
}
}
}
}
}
}
}
}
}
]
かなり長いですね....
🎬 おわりに
LISPに真似て作り始めたJSOPでしたが、LISPと同じようにマクロを使う事で容易にメタプログラミングできる点が気に入っています。ただ、Key-Value Pairで組み立てるとどうしてもプログラムが冗長になってしまい、LISPのS式のようなシンプルな構造でプログラムを組み立てる美しさはないなと感じています。FizzBuzz問題の解法を見て分かるとおり、実用的な言語ではないなと思っています。
最後になりますが
- こんな機能追加したらどう?
- こうした方がもっと良くなるよ!
など、アドバイス/フィードバックもらえると嬉しいです。コメントお待ちしております!
Discussion