YAML DSLシリーズ第三弾です。
前回までで、変数を含んだHello Worldと、文字列のフォーマットを実装しました。
今回は条件分岐を実装したいと思います。
#DSLを(少しだけ)シンプルにしておく
条件分岐を実装する前に、前回までのDSLをシンプルにします。
func: (関数名)
args: (引数)という構成をやめて、
(関数名): (引数)というスタイルにします。
変更前
- func: set
args:
- your_name
- cuhey3
- func: set
args:
- greeting
- Konichiwa!
- func: print
args:
- Hello, World!
- $: [format, 'Your name is... {}! {}', $: your_name, $: greeting]
- Goodbye!
変更後
- set:
- your_name
- cuhey3
- set:
- greeting
- Konichiwa!
- print:
- Hello, World!
- $: [format, 'Your name is... {}! {}', $: your_name, $: greeting]
- Goodbye!
ねらいとしては、funcもargsも書かなくてもわかるよね、ということと、
もうひとつは、評価関数$の記法(すでに$: [...]というような形で実装されている)に合わせたというのがあります。
では、DSLを解釈するプログラム(通称DSLランナー)を修正しましょう。
変更前(解釈部分のみ)
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)
変更後(解釈部分のみ)
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からデザインします。
- 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関数が出てきていますが、今はそれっぽいものがあればよいので、以下のように実装します。
# ログインチェックで使用するusernameとpasswordのペア
user_info = {'cuhey3': 'rumiokubo'}
# ログインチェック命令
def check_auth(username, password):
return user_info.get(username) == password
また、when関数の第二引数、第三引数は、実際には「引数として評価」ではなく、「DSLとして実行」をしてもらわないといけません。
DSLランナーの解釈処理から、実行部分をexecute_dsl関数として切り出して、子DSLを渡すと実行されるようにしましょう。
# 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関数はすぐに実装できます。
# 条件分岐命令
# 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の全量を記載します。
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を作る楽しみだと思います。
それでは、ここまでお読みいただき、ありがとうございました。