LoginSignup
5
2

More than 5 years have passed since last update.

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

Posted at

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を作る楽しみだと思います。

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

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2