👀

JSONでもプログラムを書けるらしい

2024/10/17に公開

「JSONでもプログラムを書けるらしい」

と言ったら他人事に聞こえますが、今回JSONのフォーマットに則った形でプログラムを組み立てるプログラミング言語/インタプリタを作りました。

その名も「JSOP」です。JSON Processorの略称で、何を隠そうLISP(LIST Processor)に倣って命名しました。

https://github.com/JunNishimura/JSOP

本記事は「プログラミング言語作ったよ」という以上の話はないのですが、モチベーションやら簡単な言語仕様などを説明できればと思います。こんな言語も有り得るのだなと楽しんで貰えたら幸いです。

{
    "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

📖 言語仕様

ファイル拡張子

ファイル拡張子としては

  1. .jsop
  2. .jsop.json

の2つを受け入れるようにしています。

共通

  1. 全てが式(Expression)
  2. JSONの構文に従う

Integer

単純に整数値を記述するだけです。

int.jsop.json
123

String

ダブルクオーテーションで囲まれた文字、記号、数字等を含む一連の列がStringです。

string.jsop.json
"Hello, world!"

Boolean

true, falseのいずれかです。

boolean.jsop.json
true

Array

ブラケットで囲まれており、各要素がカンマ区切りとなっているものがArrayです。上述したIntegerやStringの他にも、下にて紹介するIfやLoopも式なので、Arrayの要素として含めることができます。

array.jsop.json
[
    1,
    "hoge",
    true
]

Identifier

  1. String
  2. 先頭の文字が$

を満たすものがIdentifierです。

ident.jsop.json
"$hoge"

Identifierの文字列への埋め込みはカーリーブラケットを使うことで実現します。

embed_indent.jsop.json
"{$hello}, world!"

Assignment

代入に関してはsetキーを使うことで実現します。

親キー 子キー 説明
set 代入の開始合図
var 変数名
val 代入する値
set.jsop.json
[
    {
        "set": {
            "var": "$x",
            "val": 10
        }
    },
    "$x"
]

Function

Function Definition

関数定義ではLambda Expressionを使用します。

親キー 子キー 説明
lambda Lambda文の開始合図
params 引数(省略可)
body 関数本体
func_def.jsop.json
{
    "set": {
        "var": "$add",
        "val": {
            "lambda": {
                "params": ["$x", "$y"],
                "body": {
                    "command": {
                        "symbol": "+",
                        "args": ["$x", "$y"]
                    }
                }
            }
        }
    }
}

Function Call

関数呼び出しに関してはcommandキーを使用します。

親キー 子キー 説明
command 関数呼び出しの開始合図
symbol 呼び出し関数
args 引数(引数なければ省略可)
func_call.jsop.json
{
    "command": {
        "symbol": "+",
        "args": [1, 2]
    }
}

Builtin Functions

ビルトイン関数には下記のようなものがあります。

関数 説明
+ 足し算
- 引き算
* 掛け算
/ 割り算
% モジュロ演算
! 否定
&& AND演算
|| OR演算
== イコール
!= ノットイコール
> 大なり
>= 大なりイコール
< 小なり
>= 小なりイコール
print 標準出力へ出力
len 配列の長さを取得
at 指定した配列要素にアクセス

If

If文ではifキーを使用します。

親キー 子キー 説明
if If文の開始合図
cond 条件式
conseq condがtrueの時に実行される式
alt condがfalseの時に実行される式(省略可)
if.jsop.json
{
    "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.jsop.json
{
    "loop": {
        "for": "$i",
        "from": 0,
        "until": 10,
        "do": {
            "command": {
                "symbol": "print",
                "args": "$i"
            }
        }
    }
}

Arrayの要素に対してループ処理を実行することもできます。上の例とは異なり、inキーにArrayを指定します。

loop_in.jsop.json
[
    {
        "set": {
            "var": "$arr",
            "val": [10, 20, 30]	
        }
    },
    {
        "loop": {
            "for": "$element",
            "in": "$arr",
            "do": {
                "command": {
                    "symbol": "print",
                    "args": "$element"
                }
            }
        }
    }
]

また、break, continueは下記のようにキーとして挿入します。

loop_break_continue.jsop.json
[
    {
        "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キーを使用します。

return.jsop.json
[
    {
        "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シンボルを呼び出し、文字列の先頭にバッククォートを付ける事でアンクォートすることができます。

unless_macro.jsop.json
[
    {
        "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"
                }
            }
        }
    }
]

関数定義もマクロを利用すればシンプルにできます。

defun_macro.jsop.json
[
    {
        "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

コメントに関しては任意の箇所で//キーを使うことで挿入することができます。

comment.jsop.json
{
    "//": "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問題を解いてみた例を載せておきます。

fizz_buzz.jsop.json
[
    {
        "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問題の解法を見て分かるとおり、実用的な言語ではないなと思っています。

最後になりますが

  • こんな機能追加したらどう?
  • こうした方がもっと良くなるよ!

など、アドバイス/フィードバックもらえると嬉しいです。コメントお待ちしております!

https://github.com/JunNishimura/JSOP

Discussion