$loadと$concatが仲間に加わりました。
$loadと$concatが仲間に加わりました。これでようやくちょっとは便利な何かとして機能し始めてきたような気がします。これら2つが具体的にはどのような効果を持つのかを少し説明したいと思います。すごく雑にいうと以下の様な感じです。
- $load – 他のファイルのデータを読み込むaction(関数)
- $concat – 複数のデータ片をまとめるaction(関数)
それぞれについてもう少し詳しく説明していきます。
$load
はじめは$loadについて説明します。
$loadは別のファイルのデータを読み込む機能
$loadは以下の別のファイルの情報を読み込むactionと言いました。つまり入力として渡すyamlのファイルが分けられるようになったということです。例えば以下の様なmain.yaml,color.yamlというファイルをデータとして渡すと、
main.yaml
color: $load: ./color.yaml
color.yaml
color: cmyk: - C - M - Y - K rgb: - R - G - B
以下のような出力になります。
python main.py main.yaml color: color: cmyk: - C - M - Y - K rgb: - R - G - B
yamlだと紛らわしいかもしれませんが。jsonで出力すると以下の様な形なのでcmykの子としてC,M,Y,Kなどが存在しています。
python main.py main.yaml { "color": { "color": { "cmyk": [ "C", "M", "Y", "K" ], "rgb": [ "R", "G", "B" ] } } }
一部分だけを取り出したい場合
一部分だけ取り出したい場合はどうすれば良いでしょう?幸い$loadはjson referenceの形式での読み込みに対応しています。なので先程の例で言えばRGBだけを取り出したい場合には以下の様に書くことができます。
main2.yaml
color: $load: ./color.yaml#/color/rgb
rgbだけを取り出すことができます。
color: - R - G - B
例えば、設定ファイル全体の一部分だけを取り出して使うみたいなことができるようになります。
json referenceについて
ところでjson referenceと言うのはどういう形式かということも説明しておいたほうが良いかもしれません。雑にだけ説明します。
json referenceは、リソースのリンク先などを表す時に使います。例えばjsonschemaだったりswagger spec(OAS2.0)だったりでも使われています。簡単に言えばドキュメントの外部のリンク先とその特定のドキュメントの内部のリンク先を指す2つの階層から成り立っています。
<external area>#<internal area>
これらはそれぞれ省略することができます。外部のリンク先の方を省略した場合には現在のドキュメント(ファイル)自身、内部のリンク先の方を省略した場合にはドキュメント(ファイル)全体を表します。ちなみに内部のリンク先の指定の方法をjson pointerという形式で指定します。
両方指定した場合
// ./foo.yamlファイルの.defitions.fooの部分 ./foo.yaml#/definitions/foo
外部リンク部分だけの場合
// ./foo.yamlファイル全体 ./foo.yaml
内部リンク部分だけの場合
// 現在開いているファイル(自分自身)の.definitions.bar部分 #/definitions/bar
(~0などを使ったquoteなどの話は省略します)
相対パスの解決方法について
ちなみに$loadの相対パスの解決方法についても詳しく説明すると、例えば以下のようなネストした参照があった場合にはネストされたファイル自身からの相対パスになります。例えば以下のようなファイル構造のときに、それぞれ以下のようなファイルがあった場合には、
. ├── b │ └── b.yaml ├── c │ └── c.yaml ├── end.yaml └── start.yaml
start.yaml
a: $load: "./b/b.yaml" # start.yamlからの相対位置
b/b.yaml
b: $load: "../c/c.yaml" # b/b.yamlからの相対位置
c/c.yaml
c: $load: "../end.yaml" # c/c.yamlからの相対位置
end.yaml
end
以下のような結果になります。普通ですね。
a: b: c: end
(swagger周りでの愚痴: 全くこれとは関係ない単なる愚痴ですが。OAS(Open API Specification)2.0の頃のswagger parserはこういうような扱いではなく全てトップレベルのファイル(start.yaml)からの位置で計算するみたいな感じでした。ひどい。一方それとは別のswayはまとも。ちなみにそれに関してはissueが建てられて直していこうみたいな話になってます(正確にいうとOAS3.0までは扱いが決まっていなかったという感じ?っぽいです。元々ファイルは分割しても良い位のニュアンスで仕様自体も1つのファイルを前提にしていたりはしているようでした))
$concatについて
次は$concatについてです。これは簡単です。$loadだけでは対応できない部分を補完するためにあります。別のファイルからデータを取ってきて切り貼りしたいのですが。切りまでしかできない感じでした。複数のファイルのデータを1つにまとめて利用したいということになったら必要になるのが$concatです。こんな感じで使います。
person: $concat: - name: foo - age: 10
これは以下の様に出力されます。まとめられます。
person: name: foo age: 10
おしまい。
$load,$concatの2つを使ってできるようになること
$load、$concatの2つを使ってできるようになることは色んな所にあるデータのつまみ食いみたいなことです。これでようやく作っていた言語にも実用性の芽のようなものが見えてくる様になってきました。
ちょっとした課題
ところで設定ファイルなどのmergeのためには$load,$concatで十分なのですが。swaggerやjsonschemaなどを使った場合にはちょっとだけ困る事があったりします。jsonschemaなどでは$refという表記を使って別のリソースへの参照を表す機能があります(ここでの$refは$で始まっているけれども。現在作っているtoy言語(zenmai)の関数(action)ではありません)。複数のファイルにあるデータを$loadなどで1つファイルにまとめた場合に、この$refの位置を調整してやる必要があるかもしれません。
例えばこういう感じのときです。それぞれ別のファイルで以下の様な内容だとします。
main.yaml
definitions: foo: $load: data/foo.yaml
data/foo.yaml
type: object properties: value: $ref: ../primitive/value.yaml # ここでの相対位置はdata/foo.yamlを基準にしたもの
これを1つにマージしてしまったときにvalueの$refが指す位置が変わってしまいます。
definitions: foo: type: object properties: value: $ref: ../primitive/value.yaml # ここでの相対位置はdata/foo.yamlを基準にしたもの
マージしたタイミングで$refの位置はmain.yamlを基準にしたもの(./primitive/value.yaml)になってほしいところですが。安易に持ってくるだけだと参照先の位置がずれてしまいます。なのでもし対象がjsonschemaやswagger specなのでしたら$refを特別扱いする必要がありそうです。
実はもう1つ新たな問題というか把握しておかなければいけないことがあります。それは$loadを加えた結果、最初の読み込みファイル(entry point)の位置という情報が重要で不可欠なものになってしまうということです。これはリンク先の位置を現在のファイルからの相対パスで指定することを許可するようになったからです。パイプなどで渡されてきたデータに対する位置情報がないものなどについてリンク先の位置を決める事が事実上できなくなります。
そろそろなんとなく
そろそろ毎回試す度にコードを書くのも面倒になってきたのでコマンドを作りたくなってきました。
yaml上の言語での戻り値について
yaml上の言語での戻り値について考える事がある。だいたい以下の2つについて考えていた。
- Noneが戻り値の関数の扱い
- 戻り値の解釈の仕方
Noneが戻り値の関数の扱い
通常のpythonの関数でreturnを書かなかった場合にはNoneが返る。
def f(): pass print(f()) # None
これをyaml上での言語はどうしたら良いかという話。はじめはpythonに倣って戻り値のない関数は戻り値を持たないものとして実装していた。内部的な実装は結構泥臭くって、変換の結果がmissingという値なら変換後のyamlの値から取り除くみたいな感じのコードで if return_value is None: return missing
みたいなコードが入っていた。
ただよく考えると、JSONにはnullが存在するし(python上ではNone)、yamlでもpairのないobjectの定義はNoneになる。Noneが値として存在しているというところがむずかしい。安易にNoneとmissingを同一視してしまっていたのを止めることにした。
では、戻り値を持たない関数はどう書くかと言うと、sideeffect
というデコレータを作った。これをつけると戻り値を持たない関数になる。例えば、昨日作ったimportの機能などは全部この sideeffect
をつけることにした。
from zenmai.decorators import sideeffect @sideeffect def f(d): print("yay", d)
名前として適切かどうかは微妙なところだけれど。まぁ副作用ではあるし良いかみたいな感じ。
戻り値を捨てるような部分をどうするかというのがまだ決まっていない
ところで戻り値を捨てるような部分をどうするかというのがまだ決まっていない。例えばyaml上でimportする時には現在以下の様に書くのだけれど。
code: $import: math n: $math.floor: 10.5
ここでのcodeに意味はない。ただ、codeは$による修飾もない部分なのに消えてしまうというのは混乱を招きそうな気がする。例えば、$importなどはNoneを返すままの実装にしておき、戻り値を元のyamlに加えないような機能を持つ関数でwrapするというような考えの方が良いのかもしれない。その場合には、やっぱり1階層分全て1つの関数呼び出しと言うのは不便なのかもしれない。
$code: - $import: math - $import: string
そういえば、変数の参照をどうすれば良いのかというのが明らかになっていないかも(代入も含めて)?
戻り値の解釈の仕方
戻り値の解釈の仕方もあんまり決めていなかった。$で始まるpairは関数呼び出し(action)として扱うということは決めていたけれど、その関数呼び出しの戻り値をどうするかについては考えていなかった。もう少し具体的にいうと、戻り値として返すdictが$で始まるpairを含んでいた場合にどう解釈するかを決めていなかった。
元々の実装では、関数呼び出し(action)は一度きりという風に実装していたけれど、特にそうする必要もない気がしたので、戻り値もまた評価器による評価の対象になる用にした。具体的には以下の様なコードを書いたときの結果が変わる。
def inc(n): return n + 1 def inc2(n): return {"$inc": n + 1}
を使った以下のyamlが
n: $inc2: 10
以下の様に変わる。
n: 12
(以前は $inc: 11
だった)
たぶん、まだ実装していないけれどquoteのようなものも必要になってくる気がしている。