YAMLでDSLを作ってHello World

皆さんこんにちは。しうへいと申します。
今日はYAMLでDSLを作ってみたいと思います。

# YAMLとは
構造化データを扱うフォーマットの一種で、JSONと（ほぼ）相互変換できます。
JSONのようにYAMLをプログラム中で読み込んで、構造化データオブジェクトを復元することができます。
YAMLのフォーマットは、インデントが強制されたJSONのようなもの、と考えるとイメージしやすいと思います。
JSONとの機能上の一番の違いは、コメントが使用可能なことです。

# DSLとは
Domain Specific Language（ドメイン固有言語）の略です。
プログラミング言語の文法は一般に、あらゆる処理を記述可能なように、柔軟に設計されています。
これに対し、柔軟性が失われてもいいから、ある特定の範囲の命令を簡潔に書きたい。
そんな場合は、DSLを作成するモチベーションになると思います。

# さっそくDSLを書いてみる
DSL自体に決まりはないので、好きなように作ったらいいのですが、標準出力を行うDSLとして、一旦以下のように書いてみました。

```yaml:hello1.yml
func: print
args:
    # argsは配列として記述
    - Hello, World!
    - Goodbye!
```
これをDSLとして解釈するPythonプログラム（便宜上、DSLランナーと呼びます）を書きます。
PythonでYAMLを扱うために、PyYamlというライブラリをあらかじめインストールして、使用します。

```python3:dsl_runner.py
import yaml

# DSLから呼び出し可能な関数を列挙
func_dict = {'print': print}

with open('hello1.yml') as yaml_file:
    # YAMLのロード
    dsl = yaml.load(yaml_file)
    # ロードされたDSLを、確認のために標準出力
    print(dsl)
    # 実際の解釈処理
    if 'func' in dsl and 'args' in dsl:
        func_name = dsl['func']
        if func_name in func_dict:
            func = func_dict[func_name]
            func(*dsl['args'])
```

DSLによって渡された関数名が、事前に関数の辞書func_dictに存在していれば、そこから関数の実体を取得し、
それに引数を可変長引数として渡して実行する、という実装です。
処理自体には、関数名のprintも、引数も入っていないところがミソです。
実行結果は以下のようになります。

```
{'func': 'print', 'args': ['Hello, World!', 'Goodbye!']}
Hello, World! Goodbye!
```

# 世界で一番単純なDSLの実装
さて、何をもってDSLが実装されたとするかは難しいところなのですが、
私が考える、世界で一番単純なDSLの実装は、以下のようなものです。

- DSLには、ある一定のルールに基づいて、関数名と引数を列挙する。
- DSLランナーは、ある一定のルールに基づいて、関数名と引数を受け取る。
  - DSLランナーが、関数を実際に呼び出すことで、処理が実行される。

「ある一定のルールに基づいて」の部分を具体化させるのが、実際のデザインおよび実装タスクだと考えられます。

# もう少し書いてみる
Hello Worldに名前を含められるようにしましょう。
先にYAMLからデザインします。

```yaml:hello2.yml
# 複数の命令を実行できるように、トップレベルをリストに変更
# setは変数をセットする命令
- func: set
  args:
    # 第一引数は変数名
    - your_name
    # 第二引数は値
    - cuhey3
- func: print
  args:
    - Hello, World!
    # セット済みの変数を引数に含める
    - your_name
    - Goodbye!
```
先にYAMLから書きはじめて、使いやすそうか？使いにくそうか？を考えるのも醍醐味だと思いますが、
それはさておき、変更後のDSLを解釈できるよう、DSLランナーに対して以下のように修正を加える必要があります。

- トップレベルは辞書型でなく、リスト
- 変数をセットする命令を追加
- 命令実行時に渡す引数に変数が含まれる場合、評価してから渡す

これらの修正を加えたDSLランナーは、以下のようになります。

```python3:dsl_runner.py
import yaml

variables = {}

# 変数セット命令
def _set(variable_name, value):
    variables[variable_name] = value;

# 引数を評価する関数
def evaluate_args(args):
    result = []
    for arg in args:
        if arg in variables:
            result.append(variables[arg])
        else:
            result.append(arg)
    return result

# DSLから呼び出し可能な関数を列挙
func_dict = {'print': print, 'set': _set}

with open('hello2.yml') as yaml_file:
    # YAMLのロード
    dsl = yaml.load(yaml_file)
    # ロードされたDSLを標準出力
    print(dsl)
    # 実際の解釈処理
    # トップレベルをリストに変更。instはinstructionの意味
    for inst in dsl:
        if 'func' in inst and 'args' in inst:
            func_name = inst['func']
            if func_name in func_dict:
                func = func_dict[func_name]
                # 引数を評価
                args = evaluate_args(inst['args'])
                func(*args)
```

実行すると、以下のような結果が得られます。
変数名をセットし、評価して使用できていることが確認できます。

```

[{'func': 'set', 'args': ['your_name', 'cuhey3']}, {'func': 'print', 'args': ['Hello, World!', 'your_name', 'Goodbye!']}]
Hello, World! cuhey3 Goodbye!

```

# …実装がしょぼいぞ！
聡明な読者の方なら、上記DSLランナー側の実装のしょぼさに気づかれたかと思います。

- 引数を評価するって言っても、辞書と引き当ててるだけ。
- 常にevaluate_argsを呼んでいると、変数の上書きができない。それどころか、意図しない変数の汚染が起きる。

しかし、ここにも重要な示唆があります。
DSLは先にも書いたとおり、特定用途を満たせていればよいのです。

- 変数の評価として、辞書と引き当てる以上のことは行わない。
- 変数の再セットは行わない。 

という要件の元では、今の実装で十分です。
「特定用途」の範囲が拡がった時に、あらためて実装するとしましょう。

# DSLランナーを他の言語でも実装してみる
DSLとDSLランナーを実装することで、DSLの機能が実現できることを見てきましたが、
一度定義したDSLを、Pythonではない他の言語から、再利用することはできないでしょうか。
この場合、言語に固有のDSLランナーを書けばよいことになります。
次の例では、ブラウザ上のJavaScript実行環境をターゲットとし、開発者コンソール上でデモできるよう、三回に分けてスクリプトを実行します。
まずJavaScriptからYAMLを読めるようにします。

```js
// CDNからjs-yamlライブラリをロード
var script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/js-yaml/3.10.0/js-yaml.js';
document.body.appendChild(script);
```

次にYAMLをロードします。YAML文字列はhello2.ymlと同じものです。

```js
var dsl = jsyaml.load(`
- func: set
  args:
    - your_name
    - cuhey3
- func: print
  args:
    - Hello, World!
    - your_name
    - Goodbye!
`);
```

最後にDSLランナーを実行します。

```js
var variables = {};

function _set(variableName, value) {
    variables[variableName] = value;
}

var funcDict = {
    print: console.log,
    set: _set,
};

function evaluateArgs(args) {
    var result = [];
    args.forEach(function(arg) {
        if (arg in variables) {
            result.push(variables[arg]);
        }
        else {
            result.push(arg);
        }
    });
    return result;
}

dsl.forEach(function(inst) {
    if ('func' in inst && 'args' in inst) {
        var funcName = inst['func'];
        if (funcName in funcDict) {
            var func = funcDict[funcName];
            var args = evaluateArgs(inst['args']);
            func.apply(this, args);
        }
    }
});
```

実行結果

```
Hello, World! cuhey3 Goodbye!
undefined
```
Pythonの時と同じように、DSLを解釈して、結果を得ることができました。
以上でDSLの説明は終わりです。

# DSLは実際どこで、何に対して書く？メリットは？
私の場合ですが、フロントエンド開発において、テンプレートエンジンを使ってHTMLをレンダリングする際に使用しています。

- フォームの属性をYAMLで記述する。
  - input type / name / value / option / validation / データに応じたdisable, hiddenなど
  - 連動して動作するバリデータもあります。
- 一旦サーバーサイドでデフォルト情報などを補う。（DSLランナーその1）
- フォームオブジェクトをテンプレートエンジンのカスタムフィルタやマクロに渡す。（DSLランナーその2）

以上の手順で、実際のHTMLとして展開させることをしています。

メリットですが、まず自分が書いてて楽しいことと、
そしてチーム開発をする際にも、メンバーに既存のDSLを見せた上で、
「こうやって書いたら画面上こう動くからよろしくね、動きがわかんなかったら聞いてね」で通じるケースが多いです。
DSLがシンプルな反面、DSLランナーはどうしてもごちゃごちゃするのですが、
興味を持ってくれるメンバーは、DSLランナーの中身を読んで、拡張してくれたりもします。
いまは結構いい感じで回っているかも？

というわけで、ここまでお読みいただきありがとうございました。