YAML DSLで条件分岐を実現する

YAML DSLシリーズ第三弾です。
前回までで、変数を含んだHello Worldと、文字列のフォーマットを実装しました。
今回は条件分岐を実装したいと思います。

#DSLを（少しだけ）シンプルにしておく
条件分岐を実装する前に、前回までのDSLをシンプルにします。
func: (関数名)
args: (引数)という構成をやめて、

(関数名): (引数)というスタイルにします。

変更前

```hello2.yaml
- func: set
  args:
    - your_name
    - cuhey3
- func: set
  args:
    - greeting
    - Konichiwa!
- func: print
  args:
    - Hello, World!
    - $: [format, 'Your name is... {}! {}', $: your_name, $: greeting]
    - Goodbye!
```

変更後

```hello3.yaml
- set:
    - your_name
    - cuhey3
- set:
    - greeting
    - Konichiwa!
- print:
    - Hello, World!
    - $: [format, 'Your name is... {}! {}', $: your_name, $: greeting]
    - Goodbye!
```

ねらいとしては、funcもargsも書かなくてもわかるよね、ということと、
もうひとつは、評価関数$の記法（すでに＄: [...]というような形で実装されている）に合わせたというのがあります。
では、DSLを解釈するプログラム（通称DSLランナー）を修正しましょう。

変更前（解釈部分のみ）

```dsl_runner.py
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)
```

変更後（解釈部分のみ）

```dsl_runner.py
with open('hello3.yml') as yaml_file:
    # YAMLのロード
    dsl = yaml.load(yaml_file)
    # 実際の解釈処理
    for inst in dsl:
        for func_name, raw_args in inst.items():
            if func_name in func_dict:
                func = func_dict[func_name]
                # 引数を評価
                args = evaluate_args(raw_args)
                func(*args)
```
(微妙に)シンプルになりました。

#条件分岐も関数として実装してみる

条件分岐の例として、ログイン処理を模してみます。
usernameとpasswordの組み合わせをチェックし、ログイン成功/失敗を標準出力するようにします。
例によってYAML DSLからデザインします。

```login.yml
- set: [username, cuhey3]
- set: [password, rumiokubo]
# ログイン処理（成功する）
- when: &login
  - $: [check_auth, $: username, $: password]
  # 成功時処理
  -
    - print: 'Login Successful!'
    - print:
        $: [format, 'Welcome {}!', $: username]
  # 失敗時処理
  - print: 'Login Failed.'
- set: [password, yukaotsubo]
# ログイン処理（失敗する）
# YAMLのアンカー/エイリアスを使用して記述を再利用
- when: *login
```
前段で、評価関数$とその他の命令を同じ記法にしたので、条件分岐も同じく命令（関数名と引数の組み合わせ）として記述できると良さそうです。
上のデザインでは、条件分岐関数whenは引数を三つ取り、まず第一引数を評価して真偽値を得て、それがTrueなら第二引数の命令群を実行、Falseなら第三引数を実行、という感じです。
生のコードのif〜elif〜elseに比べると見るからに表現力が低いですが、最初の実装としてはこんなものでしょう。
パスワードを変えて、ログイン成功と失敗の両方を出力してみます。

when関数の他に、新しくcheck_auth関数が出てきていますが、今はそれっぽいものがあればよいので、以下のように実装します。

```dsl_runner.py
# ログインチェックで使用するusernameとpasswordのペア
user_info = {'cuhey3': 'rumiokubo'}


# ログインチェック命令
def check_auth(username, password):
    return user_info.get(username) == password

```

また、when関数の第二引数、第三引数は、実際には「引数として評価」ではなく、「DSLとして実行」をしてもらわないといけません。
DSLランナーの解釈処理から、実行部分をexecute_dsl関数として切り出して、子DSLを渡すと実行されるようにしましょう。


```dsl_runner.py
# DSL実行関数
def execute_dsl(dsl):
    # 命令が一つだけ渡される場合も許容する（リストとして解釈する）ように変更
    if type(dsl) is not list:
        dsl = [dsl]
    for inst in dsl:
        for func_name, raw_args in inst.items():
            if func_name in func_dict:
                func = func_dict[func_name]
                # 引数が一つだけ渡される場合も許容する（リストとして解釈する）ように変更
                if type(raw_args) is not list:
                    raw_args = [raw_args]
                args = evaluate_args(raw_args)
                func(*args)
```

ここまで用意できていれば、when関数はすぐに実装できます。

```dsl_runner.py
# 条件分岐命令
# when関数が呼ばれた時点で、第一引数は評価が終わっていて、真偽値に変換されている。
def when(predicate, true_dsl, false_dsl):
    if predicate:
        execute_dsl(true_dsl)
    else:
        execute_dsl(false_dsl)

```

実行結果です。期待通りに実行できています。

```
Login Successful!
Welcome cuhey3!
Login Failed.
```

以下にdsl_runner.pyの全量を記載します。

```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 _format(format_string, *args):
    return format_string.format(*args)

# ログインチェックで使用するusernameとpasswordのペア
user_info = {'cuhey3': 'rumiokubo'}


# ログインチェック関数
def check_auth(username, password):
    return user_info.get(username) == password


# 条件分岐関数
# when関数が呼ばれた時点で、第一引数は真偽値に変換されている。
def when(predicate, true_dsl, false_dsl):
    if predicate:
        execute_dsl(true_dsl)
    else:
        execute_dsl(false_dsl)


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


# 引数を評価する関数
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](*evaluate_args(
                    variable[1:])))
            else:
                result.append(_get(variable))
        else:
            result.append(arg)
    return result


# DSL実行関数
def execute_dsl(dsl):
    # 命令が一つだけ渡される場合も許容するように変更
    if type(dsl) is not list:
        dsl = [dsl]
    for inst in dsl:
        for func_name, raw_args in inst.items():
            if func_name in func_dict:
                func = func_dict[func_name]
                # 引数が一つだけ渡される場合も許容するように変更
                if type(raw_args) is not list:
                    raw_args = [raw_args]
                args = evaluate_args(raw_args)
                func(*args)


with open('login.yml') as yaml_file:
    # YAMLのロード
    dsl = yaml.load(yaml_file)
    # ロードされたDSLを標準出力
    print(dsl)
    execute_dsl(dsl)
```

#終わりに
条件分岐が実装できると、DSLとしてグッと表現力が増す感じがします。
こうなると同じ制御文としてforも実装したくなるところですが、
foreachはより実用的かもしれません。

どういう順番で実装して、DSLの表現力を増やしていくか？それを考えるのもDSLを作る楽しみだと思います。

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