オリジナル言語の文法について

全快の記事でちらっと出てきたオリジナル言語の話ですが、文法の特徴を紹介します。

この言語はNoCode・ユーザ定義用に作られており、現状はJSON形式をサポートしていますが、要はオブジェクトの配列が扱えればXMLだろうがなんだろうがこの言語を扱うことができます。

プロジェクトの構造はごく普通のオブジェクト指向で、1ファイルに1つのクラスを定義し、1つのクラスにはメンバーメソッド・変数を持ちます。 コードの中身はほぼ関数型言語で、基本的にはCallオペレーターでメソッドの呼び出しを並べることになります。

次がhello!world!を出力するコードです。

[{
  "@method":"echo",
  "code":[{
    "@call":"common.echo",
    "arguments":{
      "value": "hello!world!"
    }
  }]
}]

「@method」がMethodオペレーターでメソッド定義、「@call」がCallオペレーターでメソッド呼び出しになります。つまり、「common.echo」メソッド(引数valueの値を標準出力に出力)を呼び出す処理内容のechoメソッドを定義しています。

この言語はif文やfor文を極力書かないようにするため、いくつかの特別な文法を持っています。その一つにフックがあります。この言語の中心的存在です。

たとえば、カードの基底クラスを作って、スキル毎に派生のカードを作るとします。また、カードは効果の発動内容を定義したuseメソッドを持つとします。このとき、強力なカードを作るなら、コストなどの発動条件を付けたいと思うでしょう。すると、普通の言語はこう書きます。

function use ( ... ) {
  if (キャラクターのMP >= カードのコスト) {
   (カードのコストを支払う)
   (効果発動)
  }
}

このようにif文で成功と失敗を振り分けますね。ただ、私はこのプログラムに不満を持っていました。

  • 条件分岐と効果発動が混在している
  • 条件分岐はtrueかfalseを返し、その結果、効果発動を実行するかしないかの2択でしかないが、条件が複雑になると可読性が大きく下がる
  • 関数型で上を実装する場合、式がないと不便

この時、カードのメソッドの処理は、ほとんどが主体処理と条件処理に分かれていることに気づきました。そこで「フック」という機能を作りました。例の場合こうなります。

[{
  "@method": "use",
  "hook": ["card.cost"],
  "args": { "cost":10 },
  "code": [
    ...
  ]
}]

「hook」パラメータはメソッドの配列を指定します。「args」パラメータはuseメソッドのデフォルト引数です。この場合、処理順はcard.cost⇒codeパラメータになります。card.costメソッドはcostをコストとして払い、払えなければ後続のhookとcodeを実行しないメソッドです。card.costの処理内では、useメソッド呼び出し時の引数を自分の引数として使用することができます。つまり「card.cost」処理内でcostパラメータが使えます。

このようにすれば、メソッドの処理の主体はcodeパラメータに書かれており、実行条件としてcard.costメソッドによるコスト消費があるということが明確になります。

どうして引数をフックとcodeで共有できるようにしたのかというと、フックは条件だけでなく、予備動作的な役割を持つためです。たとえば、以下のように。

[{
  "@method": "use",
  "hook": ["dice.throw", "card.cost"],
  "args": { "cost":10 },
  "code": [
    ...
  ]
}]

hookパラメータに「dice.throw」メソッドを追加しました。dice.throwメソッドはダイスを振ってdice引数にその目をセットします。こうすると、useメソッドが呼び出されたからダイスを振り、その目を引数としてカードの効果が発動するようになります。もし「最悪の目を出したときにコストが倍増する」ようなルールがあれば、dice.throwメソッド内でcost引数を上書きすればよいでしょう。このように、主体を実行するための条件や動作を、フックがすべて処理するような仕組みになっています。