LoginSignup
3
0

More than 5 years have passed since last update.

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

Posted at

前回の投稿 「YAMLでDSLを作ってHello World」の続きです。

前回のおさらい

前回は、変数を含んだ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で記述する、とか…)

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

3
0
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
3
0