前回の投稿 「YAMLでDSLを作ってHello World」の続きです。
前回のおさらい
前回は、変数を含んだHello Worldプログラムを、YAML DSLを使って実装しました。
# 複数の命令を実行できるように、トップレベルはリスト
# setは変数をセットする命令
- func: set
args:
# 第一引数は変数名
- your_name
# 第二引数は値
- cuhey3
- func: print
args:
- Hello, World!
# セット済みの変数を引数に含める
- your_name
- Goodbye!
この他に、このDSLを解釈するプログラム(便宜上、DSLランナーと呼称)を用意しました。
関数名を記述するプロパティfuncと、
引数を記述するプロパティargsがあり、
DSLランナー上で、二つが組み合わされることで、命令が実行されます。
引数argsは、そのままでは文字列として解釈されるので、変数を扱いたい時は、評価のロジックを実装します。
「宣言済の変数名と同じ文字列が含まれていたら、その値に置き換える」という実装で、前回は終えました。
評価部分の実装は以下です。
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でも参照できると便利ですね。
再代入をして、正しく出力ができれば目標達成です。
# 変更なし
- 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関数を追加します。
念のためファイルの全量を記載します。
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からデザインします。
- 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にマッピングしておきます。
def _format(format_string, *args):
return format_string.format(*args)
# DSLから呼び出し可能な関数を列挙
func_dict = {'print': print, 'set': _set, 'get': _get, 'format': _format}
さらに引数評価の部分にも一箇所だけ修正を加え、evaluate_argsが再帰的に呼び出されるようにします。
# 引数を評価する関数
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で記述する、とか…)
というわけで、ここまでお読みいただき、ありがとうございました。