世間でよく使われているデータフォーマットにムカつくと、ボクの考えた最強の記法を考えたくなる。いずれ飽きるけど。

仕事ではないんだけどGW中にgRPC周りを散策してたらチンチンがイライラしてきたのでgithubに適当にぶちまけて書きかけて放置していたアレを更新かけまいかと思った。まだ更新かけてないけど。

内容としては独り言なので、誰に向けてもいない書きっぷりになってる。

◆まえのやつ

見直すとまぁイケてないな。不合理な記法だ。やりたいことも範囲も境界も不明確。馬鹿が考えてる。クソアホ。いやでもブレストみたいな気持ちで書いたものですし。頭の体操みたいなもんですし。

いったん離れてから考え直すと脳が冷えていいのかもしれない。

◆改めて抑えねばならんこと

JSONは大量データに弱くて、CSVは型が皆無かつ歴史的経緯から仕様が守られていないケースが多い。型なきCSVをExcelで開いた日にはゲロを吐かされる。バイナリだと緊急時にデータをでっちあげるとき面倒で歯が割れそうになる。YAMLはアレこの世の地獄かよ。曖昧にもほどがある。可読性も悪すぎ。

カバーしたい範囲はとても広いんだけど限度はある。だから拡張可能であることを念頭に置かねばならん。

  • メタデータとペイロードの明示的分離
  • オブジェクト型の定義
  • エディタフレンドリー、シリアライザーフレンドリー
    • エディタも人間も解釈しやすいこと。
    • 機械が処理しやすいってことは、曖昧さが少ないということで、エンコードもやりやすいし人間も書きやすい。
  • インターフェース仕様記述(IDL)
    • バリデーターが整ったらIDLにできる。
  • 大量データ
    • ファイルの最後にレコードを追記できる。連結できる。
    • 頭から読んで行って解析できる。並列処理できる。
    • フォルダにファイルを配置&参照するための仕様を作る。なぜならzipやらtarのアーカイバで固めたい。固めたまま扱いたい。
      • zipやらで圧縮されたデータから読み出したりできたい。
      • ランダムアクセスできる圧縮ありアーカイバって有名どころはzipくらいしかないんだろうか。
      • Stargzってそういえばどうなったんだろ。eStargz?なにそれ。

多少の前提、禁止事項とかあるんだけど、とりあえず細かく書かない。自分のメモで書いてるんだから。

◆データ記述

▼ペイロード

「>」でペイロードの開始。閉じ不要。ひとつのファイルに複数のペイロードを入れることができる。次のペイロード開始までにその内容を詰める。

>$hoge:"test"

「$」でペイロードに命名。「:」で値を詰める。ただし「>」直後の命名は「$」を省略可。

>hoge:"test"

順不同。

>:"test"$hoge

フィールドの命名には英数字とアンダースコアしか利用できず、数字を頭文字にすることはできない。アンダースコアのみのフィールド名も不正。先頭の文字は小文字。不自由。

だからJSONのオブジェクトとは互換性がない。JSON互換の構造は、後述するkey-value配列。

トークン間の空白改行タブ文字は入れ放題。すべて無視される。

>   
:  


   "test"  


$      hoge

好きにフォーマットすればいい。

▼オブジェクト

オブジェクトはJSONと同じようにブレース囲みでいいんだわ。オブジェクトはどうやっても閉じないと(囲まないと)ままならない。フィールド定義とは別のものなんだから「>」でやる必要ないし、むしろ混乱を引き起こす。「>」で始めて「;」で閉じる説も考えたけど、囲みの開始終了が分かりにくい。目が滑る。パーサーからしてもわかりにくいっていうかステートが増える。別の記号を使えや。

>hoge:{
  >fuga:1
  >piyo:true
}

こんなん。

オブジェクト「{}」の中にはマトリョーシカよろしくデータを入れることができる。つまりファイルのトップレベルは無名オブジェクトである。

トップレベルには明示的に無名オブジェクトを複数配置できる。無名オブジェクトが複数書かれていた場合は一つのオブジェクトとして解釈される。同名フィールドは後勝ち。ただ同名フィールドはエディタがアラート吐いてほしい。

>:{>fuga:1>piyo:true}
>:{>tinko:true>unko:"test"}

▼基本型

数値とか文字とかtrueとかfalseとか。

・時間

時間の表現にISO 8601使わざるを得ないんだけど、記号含んでて嫌だ。「-」「:」「+」「-」「.」が登場しちゃう。とはいえ時刻は文字列扱いにしたくない。

>hoge:{
  >createdAt:2022-01-01T00:00:00
  >updatedAt:2022-02-01T12:12:12+09:00
}

「:」がキツいわ。実際問題そんなに困らんけど。どうせ日時は「:」記号の後にしか出てこないから再登場しようと解釈には困らない。

基本形式は「20220510T145218+0900」だから記号が削減できるっちゃできるな。基本形式だけであってもエディタが時刻として解釈できさえすれば、表示はいかようにもできる。

エディタって言ってんのはVS CodeとかVimとかSakuraエディタとかなんでも。Webフォームとしてエディタがあってもいいし、Excelみたいなもんでもいい。

「日時です」ってプレフィックスつける説も考えたけど、キリがない。そこは人間の都合を優先してパーサーが折れたが妥当だ。

型の指定がないオブジェクトのパースは微妙にかかるな。先頭の文字で型を判別できない。「2022」まで読んでも数値か日付かわからない。しゃあないけど。

・数値

数値にしたって16進表記とか指数表現を許容せねばならんはずだった。16進はアドレス書くのに使う。アドレスはバイナリの構造体を書くのに使う。

なんか全部を妄想で考えてるからわけわかんない気分がしてくる。16進数つかうか?

っていうか1オクテットずつ区切っていいとか言い始めたらエンディアンどうこうみたいな話にならないっけ。

まぁいいやどうでも。誰も実装しないし。

数値の桁区切り文字は国によって違っていて、日本やらアメリカだと

1,000,000.5

だけど、ドットとコンマが逆である国もたくさんある。

1.000.000,5

国際単位系(SI)では

  • スペースで区切ってね
  • 小数点はドットかコンマね

ってハナシになってる。

幸いにも空白やら改行やらタブ文字をスッ飛ばして破綻しない記法であるから、SIをそのまま採用できる。

1 000 000,5

もとから空白は読み飛ばされるだけ。当然空白を入れなくてもいい。

・文字列

まず、極力エスケープをしない。改行文字(\rとか\nとか)もエスケープしたくない。でも考え始めると大変な話だったりする。

なんでそんなエスケープしたくないのって、まず書き換えが面倒だから。そしてコードをデータとして書く用事があるからっすね。コードテンプレートのジェネレーターとかスニペットとか。

Snippets in Visual Studio Code

文字列中に「"」が含まれている場合は「``」で囲めばいいってノリでエスケープいらずにする方法もあれど、「"」も「`」も含まれていたらどちらにせよエスケープ必要。必要だし、エンコーダーはシリアライズ対象の文字列に「"」が含まれているかどうかとか検査したくない。

JSONのエスケープは「\"」とかあるじゃん。

{
  "hoge":"aa\"aa"
}

これはこれで嬉しいんだけど、バックスラッシュをエスケープに使うと、バックスラッシュ自体ののエスケープも必要になる。「\\」が必要になる。

重ねエスケープ方式(造語)ってのもある。「"」を文字列中で表現するために「""」みてぇにするやつ。つまり「"」を表現したいときは囲みを含めて

>hoge:""""

になる。こりゃまず人間からして理解しづらいし、機械的にも一文字ずつ読んだときつらい。ふたつめの「"」が出てきたとき、文字列が閉じられたのかどうか判断しづらい。「\"」なら解釈がらくちんぽ。

囲み文字を増やす形式もある。ダブルコーテーションみっつで囲んじゃう。「>hoge:""""""」で空文字みたいな。でも閉じたかどうかわかりづらめ問題が残るし、文字列中に「""""」が登場する場合はどちらにせよエスケープせねばならんし。

C#は最近raw string literalってのを導入していて、「"""」で文字を囲む感じにしている。JavaScriptでいえばString.raw、Pythonはr。それぞれにワカラン殺し的な仕様がそれぞれ詰まっていて微妙だな。一度聞いて覚えられない仕様って盛り込みたくない。

Swiftはまた違って、って思いながらググったらもうすげぇ網羅している情報を見つけた。

swift-evolution/0200-raw-string-escaping.md at main · apple/swift-evolution (github.com)

もう全部まとまってんじゃん最初っから(激怒)。最終的にSwiftは「#"」で開始して「"#」で閉じる。#の数は増やしてよし。前後につく#は幾らでも増やしていい。妥当っちゃ妥当。それでもういいんじゃないの。

変数埋め込み(文字列補間)を「\#()」でやってんな。わからんでもない。ブレースにしなかったのはなぜなんだろう。

データ構造として文字列補間は必要なんだっけ?解釈はさほど難しくないだろうけど、不要な機能であるような。文字列を結合したいんなら、別の記述式として外に書いたがいいだろう。

・バージョン

semver。IDL考えるときに、バージョンが欲しかった。あとパッケージ管理に使うにもバージョンが必要。

とはいえ、もう文字列でいいかなと思う。文字列検証の仕組みはある(妄想)わけだからバージョンの表現も文字列に任せる。

▼コメント

二形態あって。コメントブロックと行コメント。

/"コメントブロックは/の後に文字列"

//行コメントは「//」で開始してCRかLFで閉じる。

複数行にまたがるコメントはコメントブロックを使用する。

コメントブロックだけにしたかったんだけど、「この行いらねぇ」みたいなときに行コメントがないと非常に困る。使い勝手悪すぎる。

改行文字に意味を持たせたくなかったんだがなぁ。口惜しや。行コメントを閉じるために改行文字が必要だったとは。JSONの仕様にコメントが無いのってそういう理由なのかもしれん。

▼メタデータ

ファイルのトップレベルにはペイロードが記述されるんだけど、メタデータも書きたい。

「>」がデータの開始だとして、メタデータの開始は別の記号を割り当てなければ苦しい。というので、メタデータの開始は「%」でいい気がする。%って%以外の何物にも使えない記号であって、逆にメタデータを意味しそうな記号とか存在しないし、じゃあ%でいいじゃん。

%でメタデータの開始。閉じる必要はない。ペイロード後にも書ける。

%hoge:"hoge"
%fuga:true
>
%

・メタアクセス

「?」でメタデータにアクセス。

%env:{
  >target:"staging"
  >port:8080
}
%endpoint:"https://example.com/api"

>hoge:{
  >target: ?env.target
}

この機能がないと設定ファイルとかパイプラインとか書くのがキツくなりがち。同じ値が繰り返し登場するケースで使う。

「?」「.」は他の記号どもと扱い違うな。区切る性質がない。それにしても空白とか改行に負けない。

%env:{
  >target:"staging"
  >port:8080
}
%endpoint:"https://example.com/api"

>hoge:{
  >target:       
? 
  env
                   .     target
}

こんな書き方しても機械は別に解釈に困らない。人間からして読みづらいけど。

▼タイプ

「@」以降に型を指定できる。

>hoge:{
  >[email protected]:"test"
}

指定できるけど、上記の例はあんまり意味ない。推論だけでやったがマシ。

・タイプ定義

トップレベルに於いてタイプを「*」から定義できる。メタデータではあるんだけども%開始にしたくない。型はメタデータのメタデータにすらなりうる。

*Book:{
  >[email protected]
  >[email protected]
}

タイプ名は必ず英大文字から始まらねばならない。数字とアンダースコアを使える。「:」以降に型を設定する。

「>」でペイロード、「%」でメタデータ、「*」でタイプの定義。あと、コメント「/"任意の文字列"」はどこにでも挿入できる。全部で4つの記号がトップレベルに現れる。

仮にSenMetaタイプの1.0.0バージョンというものがあったとしたら、それをメタデータとして使う場合こうなるイメージ。

%[email protected]"1.0.0":{
  >author:"tinko"
  >unko:true
  >createdAt:2022-01-01T00:00:00
  >updatedAt:2022-02-01T12:12:12
}

バージョンの表現が美しくないな。まぁいいや(白目)。

メタデータをhogeって名前で定義してるけど場合によりhogeは書かなくていい。無名オブジェクトでいい。順序を入れ替えても解釈できる。

%:{
  
}@SenMeta"1.0.0"

・総称

考えあぐねたけどジェネリクス(総称型)いるわ。というのと、型の定義や指定では何らかの囲み文字が必要だと思われた。Union型とか考えたときに、それは囲まないと収集つかなくなる。

幸いにも「<>」が残っているからそれを使わざるを得ない。ペイロードの開始と被る「>」記号を使うのがちょいアレだけど、「<」で開いているわけだからモーマンタイ。

*Book:{
  >title @Nullable<string>
  >isbn @<null | string>
}

どうなんだろこれ。どっちがいいんだろ。まぁいいやどうでも。Union型はTypeScriptだと「|」であって、ORっぽいからそれでいいんだと思う。

さっきから書いてた「number」ってのは

*number:<int32 | uint64 | float32>

みたいな話でしょう。もっとあるけど。数値型の細かい指定ができれば大量データを処理するときに有利なことができる。かたや、何も考えずに書くときはnumberが必要。

・enum

ほかの記法にenumがある。要らないんじゃないかと思いたかったけど要る。

*Hoge:<$hoge:10 | $poyo:20 | $huga:30>

あぁ…なんか違うな。でもこれでいいような気もする。キーバリューのセットのうちどれかひとつってことだものな。「Hogeの中のhoge」みたいに指定できないといけない。

なんで10とか20とか数字を振らなきゃいけないのかって、それは名前を変えても平気なようにですね。DBに格納したり引き出したりするのも数値でやりたい。

でも最近は、100万件とかですむレコードなら文字列で持てばいいじゃんと思っている。各レイヤや連携先にいちいちEnumのマップを持つとかやりたくない。数値が入っていても何のことかわからん。ステータスコードやらエラーコードを見せられても覚えてない。保守で面倒。

▼ファイルシグネチャ

%sen

これをファイルの先頭に書くこと。なぜ4バイト費やすのか。それはエディタと仲良くするため。JSONとかだとファイルヘッダーがないんで、エディタがJSONと解釈するのに躊躇する。バイナリデータではファイルヘッダーあるんで判別できるから便利だったじゃん。

ほいで、メタデータを続けて定義していい。

%[email protected]"1.0.0":{
  >unko:true
  >createdAt:2022-01-01T00:00:00
  >updatedAt:2022-02-01T12:12:12
}

senって名前が占有されてるわけでもなくて、フィールドの定義は後勝ちだから後ろで再定義すればsenって名前も使える。でもフィールドの定義が同名で二回された場合は警告ほしいな。

▼閉じるやつ

  • オブジェクトの{}
  • 配列の[]
  • key-valueの配列()
  • 型に使う<>

でお願いしたい。タプル型はいらないと思った。やりようはある。

key-valueの配列じゃなくてkey-value自体を表現したいんであれば、それもそういうタイプをつくりゃいい。

配列もkey-value配列も「,」がデリミタ(区切り文字)。ケツカンマはあってもなくてもいい。

/"オブジェクト"
>obj:{
  >a:1
  >b:"test"
  >c:true
}

/"key-value"
>kv:(
  "hoge":1,
  "huga":5,
  "piyo":100
)

/"配列"
>ar:[
  1,
  2,
  3,
]

ミニファイしたらこうなる。

/"オブジェクト">obj:{>a:1>b:"test">c:true}/"key-value">kv:("hoge":1,"huga":5,"piyp":100)/"配列">ar:[1,2,3,]

うん。なんてこたない。

▼型指定時のフィールド名省略

*Book:{
  >title @string
  >price @number
  >author @string
}

>@Book:{
  > "星の王子様"
  > 1500
  > "アントワーヌ・マリー・ジャン=バティスト・ロジェ・ド・サン=テグジュペリ"
}

みたいな。フィールドの順序を守ってデータを記述する。CSVと同じ。ちょいパースがキツイか。とはいえ「:」を含めるわけにもいかない理由がある。

配列がArray<T>で表現されるんだとしたら

>@Array<Book>:[
  >"星の王子様">1500>"アントワーヌ・マリー・ジャン=バティスト・ロジェ・ド・サン=テグジュペリ",
  >"ウォッチメン">3740>"アラン・ムーア&デイブ・ギボンズ",
]

こんな風に書ける。だから「:」を入れたくない。とはいえパースがきついのは違いないんで、なんらかのマークを付ける必要があるかもな。

◆エディタサポートとIDL

▼論理名と物理名

Protocol BuffersのIDLでは「=」を使ってフィールドの一意な識別子をつけるのね。その識別子があればフィールドの名前を変えても識別子が変わらんから破綻しない。変更を追える。

Protocol BuffersではIDに数値しか使えないんだけど、識別子は数値でもGUIDでもいいし、URLですらいいと思えた。(そのURLに論理名を含んだら台無し感あるけど)

「=」記号を残してあるから、それを以て物理名を別につけることはできよう。

▼属性

deprecated(廃止予定)マークが欲しい。preview(いずれ消えるかもしれない)マークも欲しい。任意のフィールドやタイプが廃止予定であるとき、クライアントがそれを知覚出来るようにマークしたすぎる。

記号を割り振ってたらキリがないから、なんらかの属性を指定できるようにするべきと思った。

*Book"1.0.0":{
  >title @string !deprecated
  >isbn @string !preview
  >unko @number !experimental !tinko
}

「!」の後ろにアレする。いくらでも設定してよい。「deprecated」とかは文字列ではない。標準仕様に属性としてキーワードが定義されていてよいと思われる。自分で定義したい場合はメタ情報にEnumでも定義してそこから読み出す。

ただ、属性とか定義し始めると各クライアントでどう扱うべきかとかまで面倒見ねばならんような気もしてくるな。拡張仕様としておけば別に痛くもかゆくもないけど。

属性に数値やら文字列を渡すとかもあり得る気がする。<>で囲んで渡すようにすりゃいいんじゃない?

*Book"1.0.0":{
  >unko @number !min<1> !max<100>
}

あぁー。バリデートの話になってくる。っていうか受け取りたい値は一つと限らないんじゃないか。っていうか属性そのものはどうやって定義を記述すればいいんだ。

属性はやっぱり拡張仕様とするべきだろうな。根源的なデータ記述から話が逸れてきている。

▼データ規約

データ検証。バリデート。

文字列項目に数値が入るのは当然おかしい。入っていたらアラートを表示していただきたい。手で書き換えた変なデータはエディタが怒ってくれないと困る。

あるいは、クライアントからサーバーにデータを送る前にクライアント側で検証したい。

基本的な検証はどうとでもなるが、例えば「x文字まで」とかの本意気のバリデート情報を連携する用事があるんならそれは独立した仕様を作らざるを得ない。例えば項目Aと項目Bの両方を見なければならない検証とか、配列の要素数制限「x個選択して下さい」とか。数値項目Aと数値項目Bの合計が10を超えてはならないとか。

それもう計算になっちゃう気がするんだけど、なんとか宣言的に書けるような仕組みが必要なんだろうな。もう誰か考えていてくれたりしないかな。

とりあえずデータ記述の仕様にはならない。拡張仕様。保留。

▼unset

NULL以外に、未設定を意味するunset型みたいなのが欲しい。API書いてるときによく思うのが、

  • 明示的にNULLが入った
  • 設定操作がされなかったからNULLになった

を読み分けられなくなるやつ困る。サーバーがクライアントからデータを受け取って永続化層を更新する操作を考えたときに、困る。「NULLにリセットするフラグ」的なフィールドを全項目に対して作らねばならなくなる。

作らない場合、更新がバッティングしたときに事故る可能性が高まる。更新履歴とか一覧表示するときに差分比較処理を書かねばならなくなる。面倒。

初期値設定をサーバーに委ねたいときもunsetっぽいものが必要と思う。IDとかね。だから型定義で利用するっていうか、その型を利用したIDL記述に利用するんだろうな。

◆書いていないこと

  • 標準仕様において定義される型
    • 標準的に必要とされるメタデータ
    • 「ファイルに記述されているペイロードの型を指定する」ようなフィールドを標準メタデータに含む
      • 型が配列であると指定した場合は「[]」で囲む必要なくレコードを書くことができる
        • CSVのようにファイルの末尾にデータを追記できる。
  • 複数ファイル間の連携
    • 例えば別ファイルに記述された型の参照とか
  • 正規表現の記述方法
    • でも正規表現を仕様に含むと、すなわち正規表現の処理エンジンが必要になっちゃうんじゃねぇかな。

◆結

何も締まってないけど。そういうことを考えてた。githubを更新しておこう。実装する気ないけど、これを読んで思ったことがあったら聞きたい気持ちする。