yamlのblockスタイルとflowスタイルの違いを楽しんだ

github.com

今日もyaml上の言語のことを考えていたのだけれど。関数(action)呼び出しの部分はflowスタイルと併用すると少し見やすくなるかもしれない。

yamlのblockスタイルとflowスタイルについて

yamlにはblockスタイルとflowスタイルの2つのスタイルがある。それについての雑な説明。

blockスタイルは以下のようなもの。

person:
  name: foo
  age: 1

よく使われているし。全てこれで書くのが正義だと思っていた。一方flowスタイルはこんな感じ。

person: {name: foo, age: 1}

ちなみに、JSONはflowスタイルのsubsetと考える事ができるらしい?(JSONは常にvalidなyamlのflowスタイル)

関数呼び出しについて

例えば、$prefixという渡された文字列の先頭にvalueという引数で渡された文字列を追加するactionがあるとする。

person:
  name:
    $prefix: foo
    value: +
  age:
    $inc: 1

person:
  name: +foo
  age: 2

になる。

この呼出しをblockスタイルで書いたのが上の例の通り。これはこれで良いのだけれど。文字列の様な単純な値を対象とするときには、$prefixの引数として使われるvalueが少し離れてしまっていてぱっと見関連のない値のように見える。これをflowスタイルを取り入れると見た目も関数呼び出しっぽくなって良い。

person:
  name: {$prefix: foo, value: +}
  age: {$inc: 1}

(ちなみに、valueというキーワード引数の名前はどうかと思うのだけれど。prefix,suffixで共通して使える何かを探そうとして失敗した)

変数束縛

ところでactionの対象(ここでは$prefixのfooなどを指している)が短い場合はflowスタイルが良いけれど。長い場合はblockスタイルにならざるおえない。変数束縛の様な機能を追加してみるのも良いかもしれない。色々記法を考えたのだけれど。名前と値のペアということに縛られた上での記法を考える必要があり、なかなかこれというものが思いつかなかった。結局、右往左往した結果、letと言うものを導入することにした(まだ実装はしていない)。こういう形式。

$let:
  <var1>: <value1>
  <var2>: <value2>
  ...
body:
  <body>

var1とvalue1のペアに束縛したい値を渡す。bodyにその環境(scope)での操作を書く。例えば以下の様な部分適用された関数を返す関数(action)の$partial$が定義されていたとして(気持ちとして関数を返す関数なので末尾に$を付けている)。以下の様に書ける。

$let:
  withPlus:
    {$partial$: $prefix, value: +}
body:
  person:
    name: {$withPlus: foo}
    age: {$inc: 1}

対象がオブジェクト(dict)の場合にはbodyを省略できたら嬉しい。

$let:
  withPlus:
    {$partial$: $prefix, value: +}
person:
  name: {$withPlus: foo}
  age: {$inc: 1}

ところで、この$letの実装は少し曲者で、引数部分(body)を評価する前に対象の部分を評価する必要がある。例えば、上の例でいうと、withPlusが束縛されていない状態で、person以下の部分の評価が行われてはマズイ。普通の関数では無理でちょっとした構文として取り扱う必要があるかもしれない。

あと、今現在の実装ではscopeという概念がないので実装する必要があるかもしれない。

scopeの話

scopeの話もちょっとだけしてみる。scopeがないとどういうことが起きるかというと、

$let:
  withPlus: {$partial$: $prefix, value: +}
person:
  name: {$withPlus: foo}
  age:
    $let:
      withPlus: {$partial$: add, n: 10}
    body:
      $withPlus: 10
friends:
  - name: {$withPlus: bar}

ここでfriends部分の$withPlusがどのような値になるかという問題。scopeがない場合には、例えば、addが使われた方のwithPlusが使われるかもしれない。もちろん以下の様な評価が行われてエラーを出す。add('bar', n=10)

あるいは、以前に導入した$loadの扱いについて、

foo:
  $load: bar.yaml
bar:
  $xxx: xxx  # いきなり$xxxと言うものが生える。

$loadによって内部で読み込まれた値がそのまま環境に束縛される場合に、いきなり$xxxという謎の関数が使える様になってしまう。それも$loadした式の後でのみ。例えば以下のような呼び出し関係になっているところで、

$load a.yaml # in main.yaml
  $load b.yaml # in a.yaml
    $load c.yaml # in b.yaml

深い$loadの呼び出しの末端部分で読み込まれた情報が使われると言うようになってはだいぶ困る(global変数)。

再びblockスタイルの話

先程の$letを使った部分、あれを全部blockスタイルで書くとすると以下の様になる。

$let:
  withPlus:
    $partial$: $prefix
    value: +
person:
  name:
    $withPlus: foo
  age:
    $let:
      withPlus:
        $partial$: add
        n: 10
    body:
      $withPlus: 10
friends:
  - name:
    $withPlus: bar

慣れれば分からなくもないけれど。flowスタイルの方が呼び出し時のキーワード引数っぽさが出てるような気がする。

そう言えば

そう言えば、ちょっとだけ機能を追加して、コマンドとして呼べるようにした。以下で動くような感じ。

$ zenmai data.yaml

何か追加でaction定義したい場合には以下の様にすればOK。

foo.py

def add2(n):
    return n + 2

data.yaml

person:
  name: foo
  age: {$add2: 10}
$ zenmai -m ./foo.py ./data.yaml
person:
  name: foo
  age: 12