◆ルートチェンジの記号違う

ルート変更が「|=」だったけど考え方違うかも。オブジェクトの閉じ免除という見方のほうが分かりやすい。パーサーからしても元の記法を活かせるんで良い。「\」で閉じ免除してみる。¥なのか\なのか。

/"誤"
|books=
/"正(配列)"
>books:[\
/"正(オブジェクト)"
>book:{\
/"バイナリ"
>bin:\[こっから終端までバイナリ]

前より自然。バイナリの前置記号がなくなったんで、配列もオブジェクトも開始されてなかったらバイナリとする。多少見づらいけど常用するもんじゃないから問題ない。

閉じ免除されている対象がオブジェクトであり、同じプロパティに値を設定したら、より下の値で上書き。閉じ免除対象が配列である場合は追加。キーを持つオブジェクト/タプルの配列への同じキーでの追加は上書き。それは閉じ免除されていてもいなくても解釈は変わらない。Keyを持つオブジェクト/タプルの扱いっていうか解釈はそれ以外の配列と異なる。

なぜ閉じを免除したいのかって言えば、CSVみたいにファイル末尾への追記で更新を終えたいから。末尾に追記できるとgzとgzを圧縮してたまま連結するようなこと出来てヤバい。つまり、gz圧縮で保存してるファイルに、ストリームから入ってきたgzをゼロコピーで引っ付けて保存を終えることができる。むろん信頼できる送信元であることが条件だけど。あと圧縮ヘッダー分の容量が増えるけど。

zstdについても、zstdとzstdを単純に連結すれば単体のzstdになるんだらしい。偉い。メモリ食うとかって話きいたけどどうなんだろ。そして圧縮したまま連結できるとはいえLZ77だかLZSSに基づいているはずで、圧縮からの連結は、連結からの圧縮より圧縮効率落ちる。共有辞書があれば助かる。ZSDCHとかzstd-dとか呼ばれてるやつ。

バイナリにしちゃうと終端までバイナリなんで、バイナリに対するTOCを動的に上書きしたい場合は外部参照(後述)で別ファイルを見るよろし。

◆日付と時刻はシングルコーテーション囲み

>a:'1999-12-31T23:59:59+09:00'
>b:'12:00'

3回くらい気絶(入眠)しながらISO 8601のWikipdia記事を読んでたんだけど、シングルコーテーションで囲めばいいやと思った。日時は表現の範囲が広すぎる。機械的に数値との読み分けをしたいし。

あんまり考えてないけどプレフィックスで書き分けもできそう。UnixTimeとかは「unix」だの「u」ってプレフィックスをつけちまえば良いとも思う。

>a:'time12:00:00'
>b:'unix1713616442'
>c:'year 2024'

解釈が楽。プレフィックスの後にはスペースを入れても入れなくてもいい。そもそもプレフィックスは省いてもいいし。型指定があるならなおさら。ただし型の指定ないまま「’05’」とか書かれても何がなんだかわからないから「’day05’」とか「’month5’」みたいにせねばならんでしょう。

なれば、型として「datetime」とか「time」とか「year」とかありうる。timeとかyearを扱うプログラミング言語はあまりないだろうからそれぞれで好きなように処理してもらえればいい。なんで「year」とか必要かって言えば、自動で入力フォームを作りたいから。バリデーションしたいから。

◆外部参照

「#」のラベルには型もオブジェクトもプリミティブも入る(束縛できる)から、外部参照の内容もラベルにツッコめばいいと思う。そもそもラベルからの参照に「?」を使ってもいたんで、その参照子「?」の後に文字列が来たら外部参照とする。

#env : ?"https://example.com/env.sen"

envにどういう値やオブジェクトが入るのかは未知。「@」で型を指定してもいい。

型をラベルに入れると以下。ユーザー定義型名が大文字スタートにしか出来ないからそこで判断つく。

#Env : ?"https://example.com/env.d.sen"

「d.sen」ってどうなんだ。d.tsみたいな。ほんで型へのラベリングは「:」でいいのか?「@」じゃないのか?とか。型が一級なら「:」で良いだろう。もう何言ってるか分からんだろうけどプロパティの定義を代入する用途も考えられる。

フィールドから値をとることもできるだろう。

#hoge : ?"https://example.com/env.sen".hoge

例えば対象となるフィールドや、オブジェクトのメタ情報のdeprecatedがtrueだったら解釈時にアラートをあげることができる。バージョン管理の話とかは知らない。DenoみたいにURLに含めればいい。

外部コードへのリンク – Deno 日本語マニュアル (yoshixmk.github.io)

っていうかラベルに入れる必要もないかも。

>book@Book:{
  >name: "たのしい絵本"
  >price: ?"https://example.com/books?id=114514".price
}

うん。これはセキュリティ大丈夫なの?用途も良くわかんねぇし。例えばExcelとかPowerAppsみたいなアプリケーションのデータソースにするときに便利なのかもな。とはいえ解釈するときに先んじて外部参照を非同期で読んでおきたいから先頭に全部まとめて欲しい感じはある。

参照先の評価のタイミングはsenを読んだ時でしかない。HTTPならExpiresとかETagとかヘッダーを見つつローカルキャッシュすればいいじゃない。

◆操作ログ

あるオブジェクトの内部から上位のメタデータを書き換えたいかもという要求について考える。

%sen@sen:{
  >author:"田中太郎"
  >updatedBy:"田中太郎"
}

>hoge:{\
>a:1
>b:"abc"
%../sen.updatedBy:"鈴木次郎"

これは良いんだろうか。なんでそんなことをせねばならんのかと言えば、「%」のメタデータ記述はオブジェクトに対して係るから。上位オブジェクトには係らない。そして末尾にPATCHして更新を完了したいから。

%sen@sen:{
  >author:"田中太郎"
  >updatedBy:"田中太郎"
}

>hoge:{\
>a:1
>b:"abc"
%../sen.updatedBy:"鈴木次郎"
>b:"def"
%../sen.updatedBy:"加藤三郎"

なんか違うな。ダサすぎた。プロパティ「b」を「def」に上書いた人物が加藤三郎だという話なんだが、1操作の纏まりの解釈の仕方が曖昧。操作単位を囲むブロックが欲しいかも。

そう。「操作」という概念が欲しい。その操作のメタデータが欲しい。操作で区切れると処理を分散させたり並列させたりした時に助かる。これこそバーティカルでいいかなもう。

%sen@sen:{
  >author:"田中太郎"
}

>hoge:{\
>a:1
>b:"abc"
|"1000","田中太郎",'2024-04-24T12:00:00',[];
>b:"def"
>a:999
|"1001","鈴木次郎",'2024-04-25T12:00:00',[];

オペレーションマーク「|」の直後に「オペレーションID」「更新者」「更新日時」「自由に使えるKeyValueの配列」を書いてもいい。その後に「;」で操作の記述を閉じる。操作にメタデータが無い場合は「|;」だけ。オペレーションマークとクロージャの間にある内容は実質的にタプル。

オペレーションマークの直後にタプルを書いてしまう記法も頭を過ったけど、なんだか違うわ。セミコロンで閉じたほうがマシ。セミコロンは配列内のデリミタと被るけど。

改行入れてるけどすべての改行は無くてもいい。

%sen@sen:{>author:"田中太郎"}>hoge:{\>a:1>b:"abc"|"1000","田中太郎",'2024-04-24T12:00:00',[];>b:"def">a:999|"1001","鈴木次郎",'2024-04-25T12:00:00',[];

パースするのもそうキツくないし格納もキツくない。オペレーション囲みは「>」「%」「*」「#」よりも強い、最高位のデリミタ。オブジェクトの次元を超えるデリミタ。

オペレーションのメタデータは考え切っては無い。オペレーションのタイプが必要かも。あとユーザーも文字列じゃなく参照とかで構わない。

▼操作

操作(オペレーション)はオブジェクトとか型とかメタデータと同じくらいの感覚で定義をせねばならんものかも。

権限制御も操作に紐づけられるものだし、バリデーションも操作に紐づく。DBの定義でバリデーションするとか無理。出来るけど破綻しないように切り詰めるコストが高めである上に、初めに厳密に作っちゃうと後々でDB定義変更が飛び交う(一敗)。

とはいえ、新たな記号を使うこともない。実態としてオブジェクトで良くて、operationという組み込み型があればよい。だって処理系からしたらオブジェクトとして扱うことしかできないのだから。operationを更に掘るとqueryとcommandになりそう。副作用ゼロである場合はquery。

*query:{
  >id @id
  >name @string;
  >params @type
  >return @type
}
*command:{
  >id @id
  >name @string;
  >params @type
  >return @type
}

>getHoge@query:{
  >
}

ちゃんと考えてないからすごく適当。バリデーションとパーミッションっていうかエラーの定義は今度考える。

オペレーションIDがあるんだから、操作結果をひとまずログ領域に書き込んでからトランザクションを確定するようなこともできるでしょうね。

◆ID設定子

フィールドやプロパティに統一的にIDを振るための記法。メタデータ「%」とか属性「!」を使えばいいかなとか思ってたけど、外からIDを使って参照するときに困るから特別扱いせねばならないわ。IDを書き換えてほしくないしメタデータとか属性に入れるのは不適。格が違う。だから「=」でID設定。

*Book="114514":{
  >title @string ="1919810"
}

Bookという型を定義しながら、ID「114514」を設定している。更にtitleというフィールドにID「1919810」を設定している。プロパティやフィールドにIDが必要な理由は、プロパティ名やフィールド名を変えてもその名称変更をトラッキングできるようにしたいから。

「=」はプロパティに適用される。それゆえ操作「|;」へのID指定は「=」じゃできない。解釈できない。タプルやオブジェクト自体にIDを設定したい場合は大人しくフィールドを生やすべし。

IDじゃなくてKeyって名前でもいいかなと思ったけどIDなんでIDにしとく。IDはidという組み込み型としても扱える。

*Book="114514":{
  >isbn @id = "aabbccdd"
  >title @string ="1919810"
}

プロパティ「isbn」はID。IDは型定義内にひとつという規約をつけたいけど、その表現力がsenには今のところなさそう。型を検査する型。

◆sendex

仕様というか、何ができるのかを考えたいやつ。用途を妄想して記法が足りているかを考えたい。

本屋にファイルサーバーがあり、APIを公開したいものとする。そして、ファイルサーバーには月ごとの販売情報を積み上げたsenファイルが存在しているとする。

APIとは言ってるけど、必ずしもWebAPIであるということでもない。データストアとしてのAPI。WebAPIでもいいし、CRUDができりゃいい。

root
 └ sen
   ├ .sendex
   ├ types
   │└ Book.d.sen
   ├ master
   │└ books.sen
   └ details
    ├ 2024年03月_売上伝票.sen
    └ 2024年04月_売上伝票.sen

「最新の月の売り上げ伝票」を引きたい時、どういう実装が可能だろうか。売上伝票は「本のID」「日時」しか持たない。

▼売上伝票の保存

「2024年04月_売上伝票.sen」があったとする。

%sen@sen:{
  >name:"2024年4月_売上伝票"
  >author:"田中太郎"
}

*Details : { 
  >book @ ?"~types/Book.d.sen"
  >datetime @ datetime
}
#b : ?"~master/books.sen"

>@array<Details>:[\
?b."9783140464079",'2024-04-22T12:00:00';

Detailsの型定義はsen/typesに置いておいても良いとして。一番意味わからん記述が以下。

?b."9783140464079"

master/books.senにはBook型のオブジェクト配列が入っている。Book型にはisbnというIDプロパティが存在している。

これは「IDが設定されたオブジェクトの配列」に対して任意のキーを指定して引いている。ということにならないかな。RDBで言えば、外部キーに加えて特定のテーブルを同時に指定しているような感じ。

あるいはこう?

?b.["9783140464079"]
?b.key("9783140464079")

とか思ったけど、idで取るんなら文字列だけでいいわ。

▼senインデックス

APIで「最新の月の情報をとる」みたいな要件があったとき、通常は多少のコードを書かねばならない。呼ぶ側で当月を見るとかサーバー側で当月を見るとか。だけどsenのインデックスファイル(.sendex)を書けばその辺の制御を静的に出来そう。

%sendex

*Details : { 
  >book @ ?"types/Book.d.sen"
  >datetime @ datetime
}

>lastMonthSales@array<Details>: ?"./details/2024年04月_売上伝票.sen"

こういうsendexが書かれていたとき、たとえば

https://example.com/sen/.lastMonthSales

このパスで最新の月のsenファイルをノーコードで引く実装を書けるんじゃないの。月が替わったら.sendexの末尾に

>lastMonthSales@array<Details>: ?"./details/2024年5月_売上伝票.sen"

という文字列をpushしてやればAPIの戻りが変わる。といった風にファイルシステムへの格納ルールでapiを記述できそう。senのapi。senpi。

ファイルサーバーのsenディレクトリ配下に本当に「.lastMonthSales」というファイルやディレクトリファイルがあったんだとしても…無視されるんじゃねぇかな。無視しないとそれはファイルシステムにAPIから生で触れてるみたいな話になる。

▼特定のISBNの本を引きたい

https://example.com/sen/master/books.sen."9783140464079"

こうなのか。なんか違う気がする。更に言えば実際は「”」がURLエンコードされて「%22」になるだろう。

▼シャーディング(暫定)

本は大量に出版されるものなので、単一ファイルに格納しようとするとえらいことになる。だからファイルを分割(シャーディング)したい。分割すれば並列処理もできる。

sen/booksをディレクトリとして、そのbooksに.sendexが置いてあることを考える。そのsendex内でシャーディングする。

したかったんだけど無理かも。

%sendex@sendex:{
  #Book@?"~types/Book.d.sen"
  >shards:[@shard<Book>:Book.isbn]
}

isbnの内容でどのように振り分けるのかのルールを書けない。同じ理由で、伝票を年月でシャーディングしたいけどできない。

なんか関数が必要になるなぁ。関数というかパスに変換する仕組み?シャードルール?

関数じゃなくてもう特有の記法を使って良いんなら、isbnプロパティについては良くある範囲指定が使えればいい。

>shards:[@shard<Book>:Book.isbn[0..3];]

これで先頭4文字目までで分割できる。けどカドカッコの用途が配列とダブってキツめ。解釈が苦しくなるってわけでも無いけど。

datetimeプロパティを持つオブジェクトについて「yyyy/mm/dd.sen」という階層で格納したいことも考えられるし。であればshardはパスの構築をするような記法にせねばならない。だから実態としてもう関数からは逃れられない。あと、日付プロパティからは「YYYY」とか「MM」とかで内容を引き出せなければならんな。

#d:Details.datetime
>shards:[@shard<Details>: ?d.YYYY "/" ?d.MM "/" ?d.dd ".sen";]

プロパティそのものをラベルに格納可能なのか…?一級なのか…?どこかで破綻しないか…?みたいな。

とにかく、sen/details/.sendexに「Details」型のシャードを書いたんで

https://example.com/sen/details/Details/2024/04

とかってパスで2024年4月の伝票を全て引ける。「2024」までなら2024年の伝票を特に頑張らず全て引ける。なぜならsenは連結に強いから。サーバー側でメタデータを消し込みつつ全部のファイルをストリームに流し込めばいい。

パスのdetails/Detailsってのが今となっては冗長なんで、/sen/.sendexにDetailsのシャードルールを書いてしまえばいい。

#d:Details.datetime
>shards:[@shard<Details>: "伝票/" ?d.YYYY "/" ?d.MM "/" ?d.dd ".sen";]
https://example.com/sen/伝票/2024/04/22.sen

こんなん。

省略せずに書いたら以下。

%sendex@sendex:{
  #d:@?"~types/Details.d.sen".datetime
  #b:@?"~types/Book.d.sen".isbn
  >shards:[
    {>shard@shard<Details> : "伝票/" ?d.YYYY "/" ?d.MM "/" ?d.dd ".sen"};
    {>shard@shard<Book> : "本/" ?b.[0..3] "/" ?b ".sen"};
  ]
}

範囲読み出しは「hoge.[0..3]」みたいにドットを打つようにしておく。というのと、やっぱり関数みたいな書き方にしないと変だわ。

(#d) => "伝票/" ?d.datetime.YYYY "/" ?d.datetime.MM "/" ?d.datetime.dd ".sen"

これなら凄く理解できる。ラベルdはshard<Details>から型がDetailsであると推論できる。バッククォート囲みで関数書けるようにするかな。

>shard@shard<Details>:`(#d) => "伝票/" ?d.datetime.YYYY "/" ?d.datetime.MM "/" ?d.datetime.dd ".sen"`

こんなん?ハイライトが無いと読めねぇな。

課題としては以下。

  • ディレクトリに個別ファイルを大量に配置するのか、単一のファイルにレコードを追記するのか堅牢に判断することができない。
  • シャーディング先が圧縮されているかどうか読めない。
  • シャーディングっていうかもうルーティングとかソーティングって名前にしたい。
  • ルーティングして分散した伝票について、IDを使って引くときにどのファイルに対象のIDがあるんだか予想付かないかも。
    • それこそIDとファイルのインデックスとして読めるsenファイルを持っておけば検索早いかも。

オブジェクトの格納ルール周りはシャーディングじゃなくタイプアソーティングとかのほうがいいだろうか。ルーティングって名前にすると他のルーティング概念と食い合ってしまう。アソーティングで。ソーティングだと他のsorting概念と語彙が食い合うからassorting。

▼アソーティングに頼る登録更新

sendexファイルにDetails型のアソーティングルールを記述した際に、例えば

https://example.com/sen/@Details

のパスに

?"master/books.sen"."9783140464079",'2024-04-22T12:00:00';

をPOSTすれば

sen/伝票/2024/04/22.sen

に追記できるかも。というのは本当だろうか。嘘に聞こえる。

なぜなら、追記するにしても対象のファイルが単一のDetailsを表現しているのかDetailsの配列なのか判断付かないから。sendexに個別ファイルとして保存するアソーティングと配列にまとめるアソーティングを記述できねばならない。あと圧縮フォーマットだな。連結可能な形式のみ許される。

%sendex@sendex:{
  >assortRules:[
    {
      >rule@func<Details, string> : `(#d) => "伝票/" ?d.datetime.YYYY "/" ?d.datetime.MM "/" ?d.datetime.dd`
      >summarize:true
      >compress@<false | "gz" | "zstd"> : "gz"
    };
  ]
}

funcってのは関数。最右のstringが戻り値で後は引数。どう実装するんだかしらない。summarizeをtrueにするとひとつのファイルに記述されたオブジェクト配列に追記して保存する。summarizeがfalseの場合は対象のファイルに上書きするんで、ユニークなパスにする必要ある。つまりGUIDとか生成してくる仕組みが必要…?

assortRules配列に入ってるオブジェクトの型を設定する必要あるかも。「@assortRule<Details>」みたいな。

funcでstringに変換するのは分かりやすいんだけど、スラッシュを書かねばならんのが冗長な気がするんでstringの配列でいいかも。

%sendex@sendex:{
  >assortRules:[
    {
      >rule@func<Details, array<string>> : `(#d) => ["伝票"; ?d.datetime.YYYY ; ?d.datetime.MM ; ?d.datetime.dd`]
      >summarize:true
      >compress@<false | "gz" | "zstd"> : "gz"
    };
  ]
}

DenoKVっぽいキー指定の雰囲気。あと変数内にスラッシュが紛れ込んだときにパスが壊れる気がしてるんで何らかのエンコーディングが必要になる。

オブジェクトを書き換えないならsummarizeすりゃいいし、書き換えたいんなら個別ファイルに分けておくのがいいだろう。summarizeするもしないも両パターン書いたって良い。それは設計するしかない。それで言えば、例えば本のデータでも書き換える部分と書き換えない部分を分けて保存するのがいいだろう。RDBみたいに。あるいはシークレットを分けて保存することもできるし、アクセス権限はファイルシステムのそれにタダ乗りできる。APIを使用するユーザーのアクセス権限を透過的に扱うなりマッピングするなりで制御できるかもね。

透過的で思い出したけどファイルシステムには透過的圧縮みたいな機能が実装されてるものもあって、保存時に自動で圧縮してくれるようなものもあるはずね。けどファイル末尾への追記がどういう扱いになるんだかしらない。

サーバーソフトウェアからしたら、sendexを指定するだけでよい。トップレベルのsendexから子のsendexを指定することもできるでしょう。

▼検索と分割

Detailsから任意のISBNを持つレコードを検索したいとき、まぁまぁ面倒かもなと思った。JSONの検索ツールとかってどうしてんだろ。全部構文解析してんのかな。

例えばgrepで指定のISBN自体見つけられるんだとしても、senの文字列は内部に何もかも入れることができるから、なんにせよファイル先頭から読んでいかねばならんのかも。ということを考えたとき、ファイルを分割するのは有効かも。「2024/05/13.001.sen」みたいに分けていいかもね。

レコード数でチャンクするとかデータ容量でチャンクするとか。レコードを毎回数えるのはカスなのでデータサイズでチャンクするんだろう。

◆省略表現

  • true⇒t
  • false⇒f

組み込み定数はこんな省略してもいいだろうけど、例えば月曜日を「’月’」で表現できるなと思った。だけどパーサーに「月」を解釈するコードなんざ書きたかないし、エディタから書き換えるんなら裏でどう持とうと関係ないし。

でも「’月’」を解釈できるプラグインとかはあっていいのかもしれない。いや、よくない。いらない。

曜日 – Wikipedia

関係ないけどDays of the Weekって長すぎるよな。youbiでいいのに。

◆考えられてないこと

  • ジェネリクスの@省略は禁止?
  • 閉じ免除されたファイルはファイル名を変えるべきか。
    • d.senのノリでob.senとかar.senとかbin.senとか。
      • png.senとかも有り得る。メタデータ付きpng。
  • binaryに設定されるmimeのattribute。
  • 制御文字を含むことができない文字列とか空白を含まない文字列とか。
    • バリデーションの領域?
  • 途中から読んでもレコードの区切りを認識できるようにRecord Separatorに意味を持たせたいかも。
    • 区切り文字 – Wikipedia
    • そうなるとRecordSeparatorをstring内でエスケープする必要が発生する。
    • あるいは、バイトポジションを示すTOCの仕様を作ればバイナリとも共有出来て便利なのかも。
      • TOCを読んでそこからレコードを読み込み始める。
  • オペレーションで型やオブジェクトのバージョン、hashを指定したいかも。
    • 外部参照により読み込む時、バージョンやhashを指定できねば面倒くさそう。知らない間にアップデートされるとキツイ。
  • Functionとか関数って呼び方だと誤解が生まれるかも。変換器。
    • いきなり記法を考え始めるんじゃなくて、まずオブジェクトとして表現できたい。
    • ガード。順序付きリストが必要?
    • 「AからBへの変換」の記法さえあれば「”月”」を食わせて月曜日を認識させることもできるだろう。
    • 型の定義の方法をコンストラクタと判定器で作る?
      • 型になんらかの原理というか根拠というか基盤が欲しい。型システムとか型理論を再勉強中。

◆結

知らない。