YAML DSLできちんと変数を扱えるようにする

前回の投稿 [「YAMLでDSLを作ってHello World」](https://qiita.com/cuhey3/items/956e16126496393bd9b3)の続きです。

# 前回のおさらい

前回は、変数を含んだHello Worldプログラムを、YAML DSLを使って実装しました。

```hello2.yml
# 複数の命令を実行できるように、トップレベルはリスト
# setは変数をセットする命令
- func: set
  args:
    # 第一引数は変数名
    - your_name
    # 第二引数は値
    - cuhey3
- func: print
  args:
    - Hello, World!
    # セット済みの変数を引数に含める
    - your_name
    - Goodbye!
```
この他に、このDSLを解釈するプログラム（便宜上、DSLランナーと呼称）を用意しました。
関数名を記述するプロパティfuncと、
引数を記述するプロパティargsがあり、
DSLランナー上で、二つが組み合わされることで、命令が実行されます。

引数argsは、そのままでは文字列として解釈されるので、変数を扱いたい時は、評価のロジックを実装します。
「宣言済の変数名と同じ文字列が含まれていたら、その値に置き換える」という実装で、前回は終えました。
評価部分の実装は以下です。

```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
```

# この実装では再代入ができない

一見良さそうなのですが、変数を再代入する際にも等しくevaluate_argsが呼び出されるので、
その際、your_nameがcuhey3と評価されたあと、_set関数が呼び出されるため、再代入できません。
（変数your_nameではなく変数cuhey3に値cuhey3がセットされる。）
YAML上では、そのままでは変数名と文字列（スカラー）が区別されません。これをうまく区別して評価を行う必要があります。

#変数評価用に特殊文字を割り当てる

今回は、以下のようにデザインしてみました。
＄: [get, your_name]のように変数名を修飾しています。
単純に＄: your_nameでも参照できると便利ですね。
再代入をして、正しく出力ができれば目標達成です。

```hello3.yml
# 変更なし
- func: set
  args:
    # 第一引数は変数名
    - your_name
    # 第二引数は値
    - cuhey3
- func: print
  # YAMLのアンカー機能を使ってみる
  args: &hello
    - Hello, World!
    # 変更箇所
    - $: [get, your_name]
    # こう書いてもいいことにする
    - $: your_name
    - Goodbye!
# 再代入
- func: set
  args:
    - your_name
    - shirogane_tsumugi
- func: print
  args:
    # YAMLのエイリアス機能を使ってみる
    # 以下には、&helloの部分が展開されます。
    *hello
```

DSLを解釈するdsl_runnner.pyの、evaluate_args関数を修正します。ついでに_get関数を追加します。
念のためファイルの全量を記載します。

```dsl_runner.py
import yaml

variables = {}


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

# 変数ゲット命令
# 今回追加した命令
def _get(variable_name):
    return variables.get(variable_name)


# 引数を評価する関数
# 今回修正した箇所
def evaluate_args(args):
    result = []
    for arg in args:
        if type(arg) is dict and '$' in arg:
            variable = arg.get('$')
            if type(variable) is list:
                # ここの実装には少し含みを持たせています。get以外の関数も呼べるように。
                func_name = variable[0]
                result.append(func_dict[func_name](*variable[1:]))
            else:
                result.append(_get(variable))
        else:
            result.append(arg)
    return result


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

with open('hello2.yml') as yaml_file:
    # YAMLのロード
    dsl = yaml.load(yaml_file)
    # 実際の解釈処理
    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)
```

実行結果は以下です。

```
Hello, World! cuhey3 cuhey3 Goodbye!
Hello, World! shirogane_tsumugi shirogane_tsumugi Goodbye!

```

無事再代入ができるようになりました。
# フォーマット機能を実装する
変数参照を入れ子にできれば、評価結果をまた別の関数に渡すことができますから、たとえば、文字列のフォーマットが可能になります。
先にYAML DSLからデザインします。

```hello2.yaml
- func: set
  args:
    - your_name
    - cuhey3
# 複数の変数を用意する
- func: set
  args:
    - greeting
    - Konichiwa!
- func: print
  args: &hello
    - Hello, World!
    # 複数の変数を評価し、さらにformat関数の引数として使う
    - $: [format, 'Your name is... {}! {}', $: your_name, $: greeting]
    - Goodbye!
# 再代入
- func: set
  args:
    - your_name
    - shirogane_tsumugi
- func: print
  args: *hello
```

フォーマット用の関数は以下のような感じです。
DSLから使用できるように、func_dictにマッピングしておきます。

```dsl_runner.py
def _format(format_string, *args):
    return format_string.format(*args)

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

```

さらに引数評価の部分にも一箇所だけ修正を加え、evaluate_argsが再帰的に呼び出されるようにします。

```dsl_runner.py
# 引数を評価する関数
def evaluate_args(args):
    result = []
    for arg in args:
        if type(arg) is dict and '$' in arg:
            variable = arg.get('$')
            if type(variable) is list:
                func_name = variable[0]
                # 修正前
                # result.append(func_dict[func_name](*variable[1:]))
                result.append(func_dict[func_name](*evaluate_args(variable[1:])))
            else:
                result.append(_get(variable))
        else:
            result.append(arg)
    return result
```

結果です。文字列のフォーマットが正しく行われています。


```
Hello, World! Your name is... cuhey3! Konichiwa! Goodbye!
Hello, World! Your name is... shirogane_tsumugi! Konichiwa! Goodbye!
```

…だいぶDSLっぽくなってきたのではないでしょうか？

# しかしながら、生コードに対するDSLの優位性って何だろう…

前回から、変数のset/get、print、formatを、DSLとして実装してきましたが、まだまだ「YAMLでDSLを書く良さ」は出ていない気がします。
「普通にコードを書く方が短く済むでしょ！」という声が聞こえてきそうです。

様々に考えられますが、今日のところは、たとえばブラウザからユーザの入力を受け取る際に、固定のフォームで以外にDSLが扱えると、夢が広がるかも。などと言っておくことにします。
（たとえば、ブラウザ上から複数のAPI URLに対してリクエストを発行して、得られたレスポンスを組み合わせる部分をYAML DSLで記述する、とか…）


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