9
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

YAMLでDSLを作ってHello World

Posted at

皆さんこんにちは。しうへいと申します。
今日はYAMLでDSLを作ってみたいと思います。

YAMLとは

構造化データを扱うフォーマットの一種で、JSONと(ほぼ)相互変換できます。
JSONのようにYAMLをプログラム中で読み込んで、構造化データオブジェクトを復元することができます。
YAMLのフォーマットは、インデントが強制されたJSONのようなもの、と考えるとイメージしやすいと思います。
JSONとの機能上の一番の違いは、コメントが使用可能なことです。

DSLとは

Domain Specific Language(ドメイン固有言語)の略です。
プログラミング言語の文法は一般に、あらゆる処理を記述可能なように、柔軟に設計されています。
これに対し、柔軟性が失われてもいいから、ある特定の範囲の命令を簡潔に書きたい。
そんな場合は、DSLを作成するモチベーションになると思います。

さっそくDSLを書いてみる

DSL自体に決まりはないので、好きなように作ったらいいのですが、標準出力を行うDSLとして、一旦以下のように書いてみました。

hello1.yml
func: print
args:
    # argsは配列として記述
    - Hello, World!
    - Goodbye!

これをDSLとして解釈するPythonプログラム(便宜上、DSLランナーと呼びます)を書きます。
PythonでYAMLを扱うために、PyYamlというライブラリをあらかじめインストールして、使用します。

dsl_runner.py
import yaml

# DSLから呼び出し可能な関数を列挙
func_dict = {'print': print}

with open('hello1.yml') as yaml_file:
    # YAMLのロード
    dsl = yaml.load(yaml_file)
    # ロードされたDSLを、確認のために標準出力
    print(dsl)
    # 実際の解釈処理
    if 'func' in dsl and 'args' in dsl:
        func_name = dsl['func']
        if func_name in func_dict:
            func = func_dict[func_name]
            func(*dsl['args'])

DSLによって渡された関数名が、事前に関数の辞書func_dictに存在していれば、そこから関数の実体を取得し、
それに引数を可変長引数として渡して実行する、という実装です。
処理自体には、関数名のprintも、引数も入っていないところがミソです。
実行結果は以下のようになります。

{'func': 'print', 'args': ['Hello, World!', 'Goodbye!']}
Hello, World! Goodbye!

世界で一番単純なDSLの実装

さて、何をもってDSLが実装されたとするかは難しいところなのですが、
私が考える、世界で一番単純なDSLの実装は、以下のようなものです。

  • DSLには、ある一定のルールに基づいて、関数名と引数を列挙する。
  • DSLランナーは、ある一定のルールに基づいて、関数名と引数を受け取る。
    • DSLランナーが、関数を実際に呼び出すことで、処理が実行される。

「ある一定のルールに基づいて」の部分を具体化させるのが、実際のデザインおよび実装タスクだと考えられます。

もう少し書いてみる

Hello Worldに名前を含められるようにしましょう。
先にYAMLからデザインします。

hello2.yml
# 複数の命令を実行できるように、トップレベルをリストに変更
# setは変数をセットする命令
- func: set
  args:
    # 第一引数は変数名
    - your_name
    # 第二引数は値
    - cuhey3
- func: print
  args:
    - Hello, World!
    # セット済みの変数を引数に含める
    - your_name
    - Goodbye!

先にYAMLから書きはじめて、使いやすそうか?使いにくそうか?を考えるのも醍醐味だと思いますが、
それはさておき、変更後のDSLを解釈できるよう、DSLランナーに対して以下のように修正を加える必要があります。

  • トップレベルは辞書型でなく、リスト
  • 変数をセットする命令を追加
  • 命令実行時に渡す引数に変数が含まれる場合、評価してから渡す

これらの修正を加えたDSLランナーは、以下のようになります。

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

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

with open('hello2.yml') as yaml_file:
    # YAMLのロード
    dsl = yaml.load(yaml_file)
    # ロードされたDSLを標準出力
    print(dsl)
    # 実際の解釈処理
    # トップレベルをリストに変更。instはinstructionの意味
    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)

実行すると、以下のような結果が得られます。
変数名をセットし、評価して使用できていることが確認できます。


[{'func': 'set', 'args': ['your_name', 'cuhey3']}, {'func': 'print', 'args': ['Hello, World!', 'your_name', 'Goodbye!']}]
Hello, World! cuhey3 Goodbye!

…実装がしょぼいぞ!

聡明な読者の方なら、上記DSLランナー側の実装のしょぼさに気づかれたかと思います。

  • 引数を評価するって言っても、辞書と引き当ててるだけ。
  • 常にevaluate_argsを呼んでいると、変数の上書きができない。それどころか、意図しない変数の汚染が起きる。

しかし、ここにも重要な示唆があります。
DSLは先にも書いたとおり、特定用途を満たせていればよいのです。

  • 変数の評価として、辞書と引き当てる以上のことは行わない。
  • 変数の再セットは行わない。

という要件の元では、今の実装で十分です。
「特定用途」の範囲が拡がった時に、あらためて実装するとしましょう。

DSLランナーを他の言語でも実装してみる

DSLとDSLランナーを実装することで、DSLの機能が実現できることを見てきましたが、
一度定義したDSLを、Pythonではない他の言語から、再利用することはできないでしょうか。
この場合、言語に固有のDSLランナーを書けばよいことになります。
次の例では、ブラウザ上のJavaScript実行環境をターゲットとし、開発者コンソール上でデモできるよう、三回に分けてスクリプトを実行します。
まずJavaScriptからYAMLを読めるようにします。

// CDNからjs-yamlライブラリをロード
var script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/js-yaml/3.10.0/js-yaml.js';
document.body.appendChild(script);

次にYAMLをロードします。YAML文字列はhello2.ymlと同じものです。

var dsl = jsyaml.load(`
- func: set
  args:
    - your_name
    - cuhey3
- func: print
  args:
    - Hello, World!
    - your_name
    - Goodbye!
`);

最後にDSLランナーを実行します。

var variables = {};

function _set(variableName, value) {
    variables[variableName] = value;
}

var funcDict = {
    print: console.log,
    set: _set,
};

function evaluateArgs(args) {
    var result = [];
    args.forEach(function(arg) {
        if (arg in variables) {
            result.push(variables[arg]);
        }
        else {
            result.push(arg);
        }
    });
    return result;
}

dsl.forEach(function(inst) {
    if ('func' in inst && 'args' in inst) {
        var funcName = inst['func'];
        if (funcName in funcDict) {
            var func = funcDict[funcName];
            var args = evaluateArgs(inst['args']);
            func.apply(this, args);
        }
    }
});

実行結果

Hello, World! cuhey3 Goodbye!
undefined

Pythonの時と同じように、DSLを解釈して、結果を得ることができました。
以上でDSLの説明は終わりです。

DSLは実際どこで、何に対して書く?メリットは?

私の場合ですが、フロントエンド開発において、テンプレートエンジンを使ってHTMLをレンダリングする際に使用しています。

  • フォームの属性をYAMLで記述する。
    • input type / name / value / option / validation / データに応じたdisable, hiddenなど
    • 連動して動作するバリデータもあります。
  • 一旦サーバーサイドでデフォルト情報などを補う。(DSLランナーその1)
  • フォームオブジェクトをテンプレートエンジンのカスタムフィルタやマクロに渡す。(DSLランナーその2)

以上の手順で、実際のHTMLとして展開させることをしています。

メリットですが、まず自分が書いてて楽しいことと、
そしてチーム開発をする際にも、メンバーに既存のDSLを見せた上で、
「こうやって書いたら画面上こう動くからよろしくね、動きがわかんなかったら聞いてね」で通じるケースが多いです。
DSLがシンプルな反面、DSLランナーはどうしてもごちゃごちゃするのですが、
興味を持ってくれるメンバーは、DSLランナーの中身を読んで、拡張してくれたりもします。
いまは結構いい感じで回っているかも?

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

9
8
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
9
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?