前にね、怒りに駆られてこんな記事を書いたんですよ。
CSVはゴミ 無能 クロス クソ クズ 悪徳 コーラン燃やし デメリット 弁護士 弟殺し オウム 事務所
詰まるところオレオレファイルフォーマット定義オナニーなんだけど、久々に見直していたら色々な思いが巻き起こったんで再構成してみる。
IETFのインターネットドラフトに投げよう。
◆2022/5追記
【再々考】テキストデータフォーマット考える
◆2020/9/14 追記
IETFだるいからgithubに書いてる。
◆エスケープされる文字
元記事では「>」「<」「&」でデータ構造を構成している。他の記号を使っていない。なるほどと思う。だけど、&じゃなくていいと思うんですよね。XMLに引きずられたわ。
他のファイルフォーマットだとこんなんなってます。
▼JSON(RFC8259)
string = quotation-mark *char quotation-mark char = unescaped / escape ( %x22 / ; " quotation mark U+0022 %x5C / ; \ reverse solidus U+005C %x2F / ; / solidus U+002F %x62 / ; b backspace U+0008 %x66 / ; f form feed U+000C %x6E / ; n line feed U+000A %x72 / ; r carriage return U+000D %x74 / ; t tab U+0009 %x75 4HEXDIG ) ; uXXXX U+XXXX escape = %x5C ; \ quotation-mark = %x22 ; " unescaped = %x20-21 / %x23-5B / %x5D-10FFFF
適当に入れたらぶっ壊れるのが「\」と「”」だな。
▼XML(W3C)
<!ENTITY lt "&#60;"> <!ENTITY gt ">"> <!ENTITY amp "&#38;"> <!ENTITY apos "'"> <!ENTITY quot """>
おなじく「<」「>」「&」「’」「”」でぶっ壊れる。
▼CSS(RFC4180)
勝手に壊れてろゴミ。
▼YAML(定義)
死ね。
▼どうするか
「<」「>」はいいとして、「&」はありえんわ。押しづらいことこの上ない。US配列でも押しづらい。「:」で良かろ。
◆プロパティの書き方
JSONみたいにプロパティ名を「”」で囲むのは違うと思う。
あと、JSONってプロパティ名が空白文字「 」とか数字とかで始まってもOKらしいんだ。そして…それって困らないか?データを扱うのは当然プログラミング言語であるわけだから、変数名として扱えるプロパティ名じゃないと困る。少なくとも俺は困る。だから、
プロパティ名を「ASCII数字」で開始してはならない。また、プロパティ名に「空白文字」「アンダースコア以外の記号」「制御文字」を含んではいけない。
という制限を作ればいいと思うね僕は。全角空白も駄目だ。全角記号は好きにすればって感じだと思う。やるやついねぇと思し、第一面倒見切れんわ。Unicodeってヤバい文字がアホほどあるからね。オートマトンこわれる。
- 記号:ASCII印字可能文字
- スペース:コンピュータにおけるスペース
実際のデータに於いては、プロパティ名の前後の空白・制御文字はトリムされる物とする。
- ASCII数字で始まっていたら構文エラー
- アンスコ以外の記号含んでても構文エラー
- 空白文字と制御文字が間に挟まっててもエラー
詳しくは後述。
んで、結果として文字列以外のデータ型は「as句」的なもので「string」「number」「year」「month」みたいに指定できるようになったね。
◆型
前の記法の弱点として「(文字列を明示的に囲っていないがために)データが文字列なのか数値なのか真偽なのか日付なのかわからん」という弱点があった。これはわりと辛い。とりあえず全部文字列と思うしかないからだ。だから、文字列については何らかの囲みが必要だろう。
じゃあ何で囲むか。「”」でもいいんだけど、でも「:」で頼む。なぜならダブルコーテーションより出現頻度少ないから。そしてその文字列は「::」で閉じてくれ。「::」を文中に含めたいんなら「:::」と書こう。詳しい記法は後述。
…と思ったんだけどやっぱ「”」でお願いします。色々考えているうちに「””」じゃなきゃダメになっちまった。
で、不安に思ったのが制御文字ね。文字列に制御文字が含まれててもいけちゃうのが強みなんだけど、セキュリティ的に不安ではある。ユーザーに変なデータぶっこまれたらレイアウトが壊れるね。
まぁ…
まぁいっか!壊れろ!
◆リスト
JSONのリストはわかり易い。えらい。パーサーからしてみても割とわかり易いと思う。
とすればパクるより他あるまい。前回の記法では全部「>」「<」「&」で解決しようとしてたんだけど、よく考えたら別にそんな縛り必要ない。
配列は「 [ ] 」で囲むことにする。配列の区切りは「,」です。
んで、JSONみたいにただ囲むのは糖衣構文とする。データ容量圧縮のために最初にプロパティ名を定義しちまう構文を提供する。ただ、大量データを扱う玄人向けであるからパンピーはJSON的記法でやっとけ。
君のような勘のいいガキは「それあればCCSVいらなくなるやん」と思ったかもしれない。その通りなんですよ。あんなん要らない。気がする。
◆オブジェクト構造の定義
structっていう型を作ればいい気がする。:structで構造体を定義。
◆以上
じゃあ実際のデータをみていこう。
◆パターン:平たいデータ
getBookなるAPIがあるもんだと思ってくれ。
JSONだと
{ "title": "ぐりとぐら", "isbn": "978-4-8340-0082-5", "genre": "えほん", "price": 900, "issueDate": "1967-01-20", "isFun": true }
こうで、それが
>title : "ぐりとぐら" >isbn : "978-4-8340-0082-5" >genre : "えほん" >price : 900 >issueDate : 1967-01-20 >isFun : true
こうなる。
文字列に改行やらタブやらを含めても平気だし、空白やら制御文字やらが適当な場所に入っても壊れない。改行文字は\rでも\nでもなんでもいい。パーサーフレンドリー。
nullを示したい時は「null」で頼む。「プロパティを書かない」はダメってことにしていただきたい。良く考えてないけど何と無く不安だから。
minify(圧縮)するとこうなるだろう。
>title:"ぐりとぐら">isbn:"978-4-8340-0082-5">genre:"えほん">price:900>issueDate:1967-01-20>isFun:true
JSONの108文字から93文字になった。この例だとそんなにだったな。
minifyのルールは「”から”(NOT””)で挟まれている部分以外の空白文字、制御文字をすべて削除」になるようにしよう。正規表現とかは後述。
自動で判別可能な型は「文字列」「数値」「真偽」「日付」「日時」でしょうね。あとnullはnullだな。
true/falseとかは「T」とか「F」でもいいだろう。nullも「N」でいいよ。ただし大文字に限る。速度重視のパーサーはそれら文字を見つけた瞬間、あらゆる文字を無視して次の「>」を探しに行ってくれればいい。
⇒ダメでした。パターンマッチング考えた時、型でswitch出来なくなる。小文字t/f/nなら良かろうと思う。
▼型あり
>title @ string : "ぐりとぐら" >isbn @ string : "978-4-8340-0082-5" >genre @ string : "えほん" >price @ number : 900 >issueDate @ date : 1967-01-20 >isFun @ boolean : true
「as」で型指定したかったけど、minify考えてたらゴチャったから「@」で指定することにする。「|」(バーティカルバー)にしようかとも思ったけど、「OR」って感じがするからやめておいた。良い記号がねぇ。
Wikipediaによると「:=」が「定義」の意味らしいが、コロン使いたくないね!
まーしかし、単なるJSON的なデータなら型推論があっからほぼ使わないでしょう。でもyearだのmonthだのを固く定義したい時は安心感が得られるね。
あとCSV様のデータ列に型を定義してやるために必要ですね。
「そも、なんのために型なんぞを定義するんだ」って話をすると、クソデータを蹴るためです(激怒)。あとExcelとかにCSV食わせた時にぶっ壊れたのが辛いからです。あと手でデータ直すときに文法チェックをかけて欲しいからです。
◆パターン:リストデータ
getBooksなるAPIがあるもんだと思ってくれ。
JSONだと
{ "books": [ { "title": "ぐりとぐら", "isbn": "978-4-8340-0082-5", "genre": "えほん", "price": 900, "issueDate": "1967-01-20", "isFun": true }, { "title": "だるまちゃんとてんぐちゃん", "isbn": "978-4-8340-0124-2", "genre": "えほん", "price": 900, "issueDate": "1967-11-20", "isFun": true }, { "title": "リアル鬼ごっこ", "isbn": "978-4-3444-0513-4", "genre": "鍋敷き", "price": 576, "issueDate": "2004-04-10", "isFun": false } ] }
こうで、それが
>books : [ >title : "ぐりとぐら" >isbn : "978-4-8340-0082-5" >genre : "えほん" >price : 900 >issueDate : 1967-01-20 >isFun : true , >title : "だるまちゃんとてんぐちゃん" >isbn : "978-4-8340-0124-2" >genre : "えほん" >price : 900 >issueDate : 1967-11-20 >isFun : true , >title : "リアル鬼ごっこ" >isbn : "978-4-3444-0513-4" >genre : "鍋敷き" >price : 576 >issueDate : 2004-04-10 >isFun : false ]
こうなる。レコード間をカンマで区切る。最後の要素のケツのカンマは省略可能。あってもいい。
圧縮!
>books:[>title:"ぐりとぐら">isbn:"978-4-8340-0082-5">genre:"えほん">price:900>issueDate:1967-01-20>isFun:true,>title:"だるまちゃんとてんぐちゃん">isbn:"978-4-8340-0124-2">genre:"えほん">price:900>issueDate:1967-11-20>isFun:true,>title:"リアル鬼ごっこ">isbn:"978-4-3444-0513-4">genre:"鍋敷き">price:576>issueDate:2004-04-10>isFun:false]
JSON349文字から301文字になった。
でだ、structを作成してみよう。
◆オブジェクト構造の定義
プロパティの形を定義する。主にリストで使用される。つまり、同じプロパティを何回も書きたくない時にのみ使用されるべき。これはCSVを置き換える目的で考えられたものだ。階層データを扱おうとすると可読性が死ぬ。エディタのサポートは必須になるだろう。
複雑になってきたなぁ。ごめんね。
▼型なし
:struct @ Book >title >isbn >genre >price >issueDate >isFun < >books @ Book[]: [ >"ぐりとぐら" >"978-4-8340-0082-5" >"えほん" >900 >1967-01-20 >true , >"だるまちゃんとてんぐちゃん" >"978-4-8340-0124-2" >"えほん" >900 >1967-11-20 >true , >"リアル鬼ごっこ" >"978-4-3444-0513-4" >"鍋敷き" >576 >2004-04-10 >false ]
うん。まぁいいんじゃないか。圧縮すると243文字になる。minifyできるCSV!すばら!
JSONが349文字なんで約70%だな。
型が定義されている場合は「プロパティ名」と「:」を削除する。しなければならない。そして、順番を守らなければならない。
あと、定義名は大文字で始めないとエラー。つまり「book」だとエラー。
CSVに近めの書き方をすると以下のようになる。
:struct @ Book >title >isbn >genre >price >issueDate >isFun < >books @ Book[]: [ >"ぐりとぐら">"978-4-8340-0082-5">"えほん">900>1967-01-20>true, >"だるまちゃんとてんぐちゃん">"978-4-8340-0124-2">"えほん">900>1967-11-20>true, >"リアル鬼ごっこ"">"978-4-3444-0513-4">"鍋敷き">576>2004-04-10>false ]
あぁ^~いいっすねぇ^~。
▼型あり
struct定義時に型も指定してやる。
:struct @ Book >title @ string >isbn @ string >genre @ string >price @ number >issueDate @ date >isFun @ boolean <
うん。
▼null許容
:struct @ Book
>title @ string
>isbn @ string
>genre @ string
>price @ number
>issueDate @ date
>isFun @ nullable - boolean
<
うん。「isFun」に「@ nullable – boolean」が入ってる。
どんな意味があるかっていうと、まずデータ生成時にエラーを吐けるし、データを受け取ったときにエラー判定できる。エディタがあればアラートを出すこともできよう。
各レイヤでのエラーの取り扱いを定義してやるべきだろうか。
▼null非許容のデフォルト値
無いと困る。
:struct @ Book >title @ string >isbn @ string >genre @ string >price @ number : 0 >issueDate @ date : 2000-01-01 >isFun @ nullable - boolean <
まぁ…妥当?しかし用途が良く分からなくなってきたな。
▼集合の要素
ある値をキーとして重複を許可しないデータ。
structではなくsetelemで定義する。「Set Element」でセテレム。
:setelem @ Book
>title @ string
>isbn @ key - string
>genre @ string
>price @ number
>issueDate @ date
>isFun @ nullable - boolean
<
「isbn」に「@ key – string」が入ってますね。文字じゃなくて数値でもいいし、なんならDateでもいいんじゃないか?よくわからんけど。
keyはnullと空文字が不可。ただし「@ nullable – key – string(非推奨)」でnullと空文字を許可する。そして、nullable key列でnullや空文字が重複してもエラーとしない。データ構造おかしいとしか言えないし、もはや集合とは呼べないけどデータ構造おかしいシステムは結構存在する。エディタは「nullable keyっておかしいよ」って注意をしてあげてもいいだろう。
nullがnullに一致しないのはいいんだけど、空文字は空文字と一致するよね。よのなかどうなっとるんかのう。
「@ key」は複数の項目に設定可能。パーサーこわれる。
◆パターン:階層データ
getBookなるAPIで、著者の情報も付いてくるようになりました。
※複数著者はとりあえず考えない。
JSONだと
{ "title": "Rainbow Fish", "isbn": "978-3-3140-1544-1", "genre": "えほん", "price": 1944, "issueDate": "1992-01-27", "isFun": true, "author": { "name":"Marcus Pfister", "birthdate":"1960-07-30" } }
こうで、それが
>title : "Rainbow Fish"
>isbn : "978-3-3140-1544-1"
>genre : "えほん"
>price : 1944
>issueDate : 1992-01-27
>isFun : true
>author :
>name : "Marcus Pfister"
>birthdate : 1960-07-30
<
こうなる。「<」で閉じるだけ。
◆外部キー
階層構造持ったデータって、データ重複がありえる。ある二冊の本があったとして、その著者が同じ場合はJSONだと著者情報を二回書かねばならぬ。あるいは「本」と「著者」でAPIを二回投げねばならぬ。
エコじゃないね。省エネでいこうぜ…
▼外部キーなし
ちゃっかり階層オブジェクトの定義の仕方も紹介する。
:setelem @ Book >title @ string >isbn @ key - string >genre @ string >price @ number >issueDate @ date >isFun @ boolean >author @ >name @ string >birthdate @ date < < >books @ Book[]: [ >title : "銀河英雄伝説 1 黎明編" >isbn : "978-4-4887-2501-3" >genre : "大河SF" >price : 864 >issueDate : 2007-02-21 >isFun : true >author : >name : "田中 芳樹" >birthdate : 1952-10-22 < , >title : "銀河英雄伝説 2 野望篇" >isbn : "978-4-4887-2502-0" >genre : "大河SF" >price : 864 >issueDate : 2007-04-25 >isFun : true >author : >name : "田中 芳樹" >birthdate : 1952-10-22 < ]
authorがどっちも田中ってひとだから情報が重複しちまってるね。
じゃあ外部キーを作ってみよう。
※オブジェクト階層があったんだとしても、すでに定義があるんなら「[プロパティ名]:」を省略すればいいよ。
▼外部キーあり
外部キーっつったけどFKじゃなくてidとさせていただく。
あと名前がIDになっちまってるけど勘弁な!
:setelem @ Book >title @ string >isbn @ key - string >genre @ string >price @ number >issueDate @ date >isFun @ boolean >authorId @ Person - id - string < :setelem @ Person >name @ id - string >birthdate @ date < >books @ Book[]: [ >title : "銀河英雄伝説 1 黎明編" >isbn : "978-4-4887-2501-3" >genre : "大河SF" >price : 864 >issueDate : 2007-02-21 >isFun : true >authorId : "田中 芳樹" , >title : "銀河英雄伝説 2 野望篇" >isbn : "978-4-4887-2502-0" >genre : "大河SF" >price : 864 >issueDate : 2007-04-25 >isFun : true >authorId : "田中 芳樹" ] >authors @ Person[]: [ >name : "田中 芳樹" >birthdate : 1952-10-22 ]
「[オブジェクト名]-id-[型]」という記法だ。idの前にオブジェクト名が無いとID扱いされちゃうから注意な。
そして、対象(参照先)となるオブジェクトにはidが設定された項目ひとつが無ければならない。あと、型が一致していなければならない。そしてidというものは重複しないため、setelemでのみ定義される。
複数idはダメ。アイデンテイファイアー って言ってんだろ!
//TODO 複数キーはまた別に考える。
参照されているIDがデータ内に含まれない場合はエラーだろうな。多分。あと、循環参照も取り敢えずエラーにしとこう。
1対1参照制限をつけたい時はkeyを一緒につけてやってくれ。
EnumとかKeyValueを表現したい時は、このIDでどうにかしてくれ。
これをCSVっぽく書くと
:setelem @ Book >title @ string >isbn @ key - string >genre @ string >price @ number >issueDate @ date >isFun @ boolean >authorId @ Person - id - string < :setelem @ Person >name @ id - string >birthdate @ date < >books @ Book[]: [ >"銀河英雄伝説 1 黎明編">"978-4-4887-2501-3">"大河SF">864>2007-02-21>true>"田中 芳樹", >"銀河英雄伝説 2 野望篇">"978-4-4887-2502-0">"大河SF">864>2007-04-25>true>"田中 芳樹"] >authors @ Person[]: [ >"田中 芳樹">1952-10-22]
はああああっ!!(達成感)あああああ!!良い!良い!良い!(自画自賛)良いよ!(押し売り)
でも良すぎて不安になってきた。コレだれか考え付いてるんでないか?こうあるべきだろ本来。似たようなデータフォーマットがあって、それが普及してないだけなのかなぁ。
とりあえず圧縮!
:setelem@Book>title@string>isbn@key-string>genre@string>price@number>issueDate@date>isFun@boolean>authorId@Person-id-string<:setelem@Person>name@id-string>birthdate@date<>books@Book[]"[>"銀河英雄伝説 1 黎明編">"978-4-4887-2501-3">"大河SF">864>2007-02-21>true>"田中 芳樹",>"銀河英雄伝説 2 野望篇">"978-4-4887-2502-0">"大河SF">864>2007-04-25>true>"田中 芳樹"]>authors@Person[]:[>"田中 芳樹">1952-10-22]
うん、おいしい!そして軽い!
元記事のあのデータ構造クソすぎんだろ。どこのアホが考えたんですかね。
◆minify(圧縮)
正規表現置き換えで
("(?:[^"]|"")*"|[^\s"]+)|\s+
を
$1
で置き換えて差し上げろ。お手軽ぅ。ただ、「\s」で改行やらタブやらを拾わない正規表現エンジンの場合は調整が必要っすね。VS Codeでは置き換え失敗したわ。
「\s」の部分を「[\s\r\n\t]」とかに書き換えれば大概通用するだろう。
「U+2028」?「U+2029」?なんのこったよ。
空白文字の定義はJSの\sをそのまま流用すればいいだろうな。
※メモ:「.」は改行を拾わないケースが多いんで、「[.\r\n]」という書き方をした。それをVS Codeで動確をしたんだけど、バグかわからんが「[ ]」の中に打った「.」がスペースとタブを拾わなくなった。何なんだろ。怖いから「[\t\r\n\s]」と明示しておけば安全かもしれない。
◆メタデータ
:structだの:setelemだのって定義してきたが、似たような感じで色々なものを定義できるだろうね。例えばメタデータは:metaで定義すればいいだろう。そしてmetaの構造をイイ感じに標準化していただきたい。
データ生成日時とかデータ名とかね。だってファイルのプロパティ開いて情報読みるの辛いじゃないか。
◆定義の説明
インテリセンス効かせてぇ。だから説明をつけよう。
:setelem @ Book !"本" >title @ string !"書籍名" >isbn @ key - string !"ISBN-13" >genre @ string !"ジャンル" >price @ number !"価格(円)" >issueDate @ date !"出版日" >isFun @ boolean !"面白いかどうか" >authorId @ Person - id - string !"著者"< :setelem @ Person !"人" >name @ id - string !"著者名" >birthdate @ date !"誕生日"<
「!」の後に説明を書いてあげよう。「!」と文字列の間に空白入れてもいいよ。好きなだけ入れていいよ。
◆不変
データの不変性。
例えばExcelにデータ食わせるときにあると便利。そもそも変更していいデータなのかどうかをクライアント方に通知したい時もあろう。
つまり、「この形式のデータを食わせたらフォームを出力できる」レベルの情報量をクライアントに渡したいよね。
:setelem @ Book
>title @ string
>isbn @ immutable - key - string
>genre @ string
>price @ number
>issueDate @ date
>isFun @ boolean
>authorId @ Person - id - string<
:setelem @ Person
>name @ id - string
>birthdate @ date<
immutableっすね。そのまま。idはハナから不変。
で、プロパティ名が省略されてても型と属性は指定できるから、CSV的記法でもimmutable指定できるね。
immutableにするルール(他の項目とバインドする)を指定できるようにしたいなぁ。
◆正規表現
バリデーションのはなしをしようと思ったんだけど、そのためには正規表現のはなしをせにゃならんなと思った。
:regex
>hogehoge : "^.*$" !"ほげほげ正規表現だよ"
>hoge : "^(foo|bar)+$"gim !"ほげ正規表現だよa"
<
こんな感じかなと。regex.hogehogeで呼び出せるみたいな。さっきみたいに「!””」で説明つけられるね。
フラグ指定は末尾につけたれ。
で思ったんだけど、これはもう定数の定義ですよね。
◆定数
:const
>foo : 100 !"foo定数だよ"
>bar : 1970-01-01T00:00:00 !"UNIX時間のはじまり"
<
正規表現は:constでは定義しない。:regexでわける。なぜなら正規表現の文法バリデーションかけたいから。
呼び出し方はconst.hogehogeだろうか。
正規表現とか定数はデータに含んで欲しくねぇなぁ。とりあえず構造体の定義とかバリデーションとか、「:」で始まるやつらでだけ使用可能にしとこう。
あとスクリプトからは当然参照可能だよね。
読み込んだ後に
[オブジェクト].$meta.const.foo
的に読めばいいかな。言語によるだろうけどね。
と書いたときに衝撃が走ったんだけど、$とか普通は駄目だよね。だから予約語を一つ作っておこう。
◆予約語
説明の順序ぶっ壊れてるけど、あとで清書するから許して亭ゆるして。
えー。予約語は「_」です。アンスコ単体。アンスコ単体のプロパティ名は禁止。プログラムがこのテキストフォーマットをオブジェクトに落とした後で何らかのメタデータにアクセスしたい時は「_」から参照する。
[オブジェクト]._.const.foo
こうだぞ!
このあとバリデーションについて話すんだけど、オブジェクトのバリデートをかけるときは
[オブジェクト]._.validate
こうだぞ!
つまりメタデータアクセスやメソッド呼び出しをこの「ポート(仮名)」から行う。
◆バリデーション
そしてバリデーション。文字数とか数値の桁数とかのチェックしたいね?
nullableもバリデーションと言えなくもねぇけど、とりあえず別で考えよう。必須(Required)はnullableで指定してあげてな。
:regex >isbn : "^\d{3}-\d{1}-\d{4}-\d{4}-\d{1}$" !"実際もっと複雑" < :const >foo : 100 < :setelem @ Book !"本" >title @ string !"書籍名" >isbn @ key - string !"ISBN-13" >genre @ string !"ジャンル" >price @ number !"価格(円)" >issueDate @ date !"出版日" >isFun @ boolean !"面白いかどうか" >authorId @ Person - id - string !"著者"< :setelem @ Person !"人" >name @ id - string !"著者名" >birthdate @ date: !"誕生日"< :validation @ Book >{.getLength(title) > /const.foo} @ title !"タイトルは100字以下で頼む" >{.getLength(title) < 0} @ [title, isbn] !"タイトルは1字以上で頼む" >{price > 1200} !"価格は1200円以内で頼む" >{isbn = /regex.isbn} !"ISBNが形式に沿ってないです" >{issueDate < authorId.birthdate} !"生まれる前に書いちゃってます" < :validation @ Person >{birthdate<1990-1-1; birthdate>2000-1-1} !"複数条件バリデーション" <
関数みたいなもんだから「{}」で囲ってみました。性質が全然違うから記法を合わせる意味はないね。そして、これは無名バリデーションです。バリデーションに名前つけてやりたい時は、「[なまえ] : {バリデーション}」でいいよ。
いきなり出てきた「.getLength(title)」は文字数を取得してるんだけど、後で説明するわ。
エラーを発火するための条件を書いてくれ。複数条件は「;」でセパレート。エラメは、それ自体が説明みたいなもんだから「!””」で指定しちくり。
@でアラートの表示先を指定。[,]で複数も可能。@を省略したら引数のフィールドにアラートを出す。
条件中の「<」とか「>」は、項目の型により動作が異なる。数値ならその大きさ。日付や日時なら未来過去だな。一致は「=」。正規表現の一致も「=」。部分一致は…とりあえず考えない。
で、問題がありまして、上の例だと治ってるんだけど、正規表現定義とかを最初は「regex.isbn」って読みだしてた。でも例えば「const」だの「regex」を項目名に持つオブジェクトが困る。あんまねぇと思うけど。そして「:const」で参照しちまうと、プロパティの「:」と区別がつかねぇから避けたい。結果として「/」を先頭につけることにした。「/」は「ルート」っぽいしね。
で、困ったのが例えば「当日以降であること」みたいなバリデーションだ。
取り敢えず考えることを保留しておくけど、「フレームワークな関数セット」を定義してやる必要があるような気がするね。それは切り分けよう。
▼リモートバリデーション
APIでサーバーにバリデーションしてもらおう。
:remoteValidation @ Book >title : "http://hoge.hoge/api/Book/validateTitle" !"タイトルは100字以下で頼む" >[title, price] : "http://hoge.hoge/api/Book/validateTitleAndPrice" !"ほげほげ" >"http://hoge.hoge/api/Book/validate" !"レコードごとバリデート" <
プロパティを省略したらレコード全体を送信。「!””」はデフォルトエラーメッセージ。
urlにデータ投げてバリデート(検証)してもらう。POSTで送るわけだからエラー文字列を受け取ることもできよう。こんなデータが返ってくればいいね。
>[title] : !"タイトルは100字以下で頼む" >[title, price] : !"ほげほげ" >!"全体に表示"
エラメ表示対象のカラム名が入っていなかったら全体な場所に表示してくれ。
時間の制御(debounce)はどうしようか。それはさすがにクライアントに頼むか。
▼親の項目でバリデーション
さっきやってたし。よくみろ。
◆サジェスト
サジェスト。入力可能なデータや入力が推奨されるデータのサジェスト。
:setelem @ Book !"本" >title @ string !"書籍名" >isbn @ key - string !"ISBN-13" >genre @ string !"ジャンル" >price @ number !"価格(円)" >issueDate @ date !"出版日" >isFun @ boolean !"面白いかどうか" >authorId @ Person - id - string !"著者"< >authors @ Person[] - id - string !"著者たち"< :setelem @ Person !"人" >name @ id - string !"著者名" >birthdate @ date: !"誕生日"< :suggestion @ Book >title : [>"はらぺこあおむし",>"はらぺこゴミむし",>"はらぺこクソむし",>"おどりゃクソ森"] >authorId : authors < :remoteSuggestion @ Book >genre : "http://hoge.hoge/api/Book/getSuggestion" < >authors @ Person[]: [ >"田中 芳樹">1952-10-22,>"田中 芳樹2">1952-10-23]
こんなん。
で、この「authors」みたいにオブジェクトをサジェストしたいこともあると思うんだけど、その辺の加工の仕方は選択リストのところで話す。
リモートサジェストはレコードをPOSTする動作になる。だろうな。サジェストの取得結果はレコード全体に及ぶ。
例における「genre」はサジェストのトリガー項目。genreの値が変更されたときに全体のサジェストが更新される。複数項目をトリガーにしたい時は「[title, genre]」みたいに囲ってくれ。
取得された値に既存のサジェストを上書くものが無かった場合、サジェスト情報を上書かない。だからremoteSuggestionに複数項目を指定しても大丈夫。
全てのサジェストをリモートで先に取得したい時はプロパティ名を省略してURLだけ書いてくれ。
帰ってくる値は
>authors : [>"田中 芳樹">1952-10-22,>"田中 芳樹2">1952-10-23] >genre : [>"えほん",>"大河SF",>"えろほん"]
とか。プロパティ名と配列が返ってくるだけ。オブジェクトでもいいよ。
◆セレクトルール
この世にはセレクトボックスというものがある。日本語名は多分「選択リスト」。コンボボックスとかもありますねぇ!
サジェストは提案だけど、セレクトは「この中から選んでね」だ。
機能としては
- 数の制限
- 順位付け
- リレーショナルなセレクトボックス
- オブジェクトからのセレクトオプションの生成
が必要っすね。
で、idでレコードを引く仕組みがあればリレーショナルなセレクトボックスも作れると思った。が、それもうクエリ言語じゃんね。filterとmapは少なくとも必要になってしまった。やっぱメソッド作らな…。メソッドっつうか関数セットか。
◆関数
えー。どうしよ。考えたくない。クソまみれになる気がする。
しかしDateTime.Now()とか使えねぇとなぁ。誕生日から年齢計算することすらできない。
なんか、このデータ記法の実装レベルみたいなのを作ったほうがいい気がするな。関数のコンパイラ書かせるのは幾ら何でもやりすぎだ。標準関数とかどっから持ってくるのって感じだし、関数はオプション扱いにしよう。そうしよう。段階として、基本の「core」と、keyとかnullableとか使える「set」と、関数とかバリデーションとか使える「function」だろか。
add()という加算結果返却関数がすでに用意されていたとしよう。
関数はアロー記法での定義になるだろうから、2を加算する関数は
:functions
>add2 : {(a @ number) => .add(a, 2)} !"2をたす"
<
だろうね。関数定義は「{ }」で囲もう。引数を囲んでるパーレンは省略可。戻り値の型も推論してエラー吐けるから省略可能。
「{ }」で囲まれた中で、関数を一つだけ定義する事ができる。入れ子なら複数の関数を定義することも可。
できることなら「>」記号を使いたくねぇんだけどやむを得まいて。踏襲しなきゃはぶられる。
標準関数は「.」から読みだす。最初は「_.」にしようかと思ったけどアンスコ要らないね。
▼中置記法
でね?、さっきも「{ }」出てきたと思うんだけど
:validation @ Book
>{title < 0} @ [title, isbn] !"タイトルは1字以上で頼む"
<
これね。中置記法してんね(戦慄)。いかんでしょ。やめ…やめよう。いや、やめたら
:validation @ Book
>{>(title, 0)} @ [title, isbn] !"タイトルは1字以上で頼む"
<
こうなるわけだが。これは無茶ですよね。ごめんなさい。
関数を中置したいときはパーレンで囲んでください。
:validation @ Book
>{(title > 0)} @ [title, isbn] !"タイトルは1字以上で頼む"
<
おっけおっけ。で、構造体に紐づくメソッドは引数を必ず省略してフィールドから読み込む。構造体に紐づかない野良メソッドは引数を指定すること。
普通の関数を中置したいときは…
:functions
>add2 : {(a @ number) => (a |.add| 2)} !"2をたす"
<
こうかな!(諦め)
バーティカルで囲んでくれ。これで多分解析できるでしょう。汚ねぇ構文だなあ。解析むずいし。そろそろ死人が出ますね。バーティカルで囲んでいたとしても普通の関数みたいに読み出していいよ。
▼オペレーター
「(a+b)」は「.add(a,b)」と同じことだ。そう読み替えてくれ。比較演算子の「>」は「.testLeftGreatness」とでも言おうか。英語わからん。
▼同名関数
加算関数「.add」「+」はアドホック多相です。あ、わかんないですよね。つまりポリモーフィズムです。あ、わかんないですよね。つまり多相性です。多相性はつまりポリモーフィズムの事です。
文字列の足し算と数値の足し算がどっちも「+」でできるって事だ。文字と数値で「>」比較したら文字数比較で、文字文字比較したら文字コードで。
▼関数読み出し
日付取得とか、関数の読み出しは以下の感じか。
:functions
>add2 : {(a @ number) => .add(a, 2)} !"2をたす"
>add2add2 : {(a @ number) => .add(/functions.add2(a), 2)}
>getYearDiff : {(a @ date) => (.now - a)}
<
関数名はクッソ適当。にしてもgetAge関数欲しいね。まぁいいや。
これじゃあんまりなんで「/functions.」のショートハンドが欲しい気がする。あと、パイプライン演算子「|>」だろう。無いと死ぬ。
▼パイプライン演算子
:functions
>add2 : {(a @ number) => .add(a, 2)} !"2をたす"
>add2add2 : {(a @ number) |> /functions.add2 |> {(x) => (x + 2)}}
<
うんうん。いや、よくねぇけど俺もう知らね。
パイプラインの左辺から値を受け取り、関数を適応して右に流す。以上。左辺から受け取った値へのアクセスは「_」から。つまり
{(x) => (x + 2)}
は
(_ + 2)
と書いていい。
▼部分適用
あと、話ちょっと戻るけど、and演算関数「.computeAND」の中置記号「&&」があったとすると
:functions
>testRange: {(a @ number) |> (a >= 0) |> &&(a <= 100) } !"0以上100以下"
<
が可能だろう。関数は引数(アリティ)が足りないことを許容する。引数が足りなかった場合、足りていない引数を引数とした新たな関数が生成される。
あと、関数への引数「a」のスコープは関数全体に及ぶと思う。「a」はつまり、「数値を受け取って、その数値をそのまま返却する関数」の定義とみなせる。と思うんだけど。まぁいいや。
▼条件分岐
パターンマッチング(ガード)は、いわゆるswitch-caseでどうにかする。記号を「#」としてみる。
:functions
>limit : {
(a @ number)
# (a < 0) -> 0
# (a > 100) -> 100
# -> a} !"範囲に収める"
>hoge:{
(a @ number)
# 1 -> 0
# 0 -> 1
# -> 0}
<
うん。悪くない。「#」のあとにBooleanを返す関数やら値を書いてくれ。関数だったら始めの引数が適用される。「->」の右側も当然関数でもよかろう。
「#」のあとが関数でなかった場合、「.testEquality」が自動で呼ばれる。これは標準関数であるから、自前での定義は不可だ。つまり構造体同士の比較では省略表記はできない。
あと、いわゆる「break」が無いから、複数ケースで同じ処理をしたいときは「->」以降を書かずに「#(a > 0) # (a < 100)」みたいに並べてくれ。
そして、caseな関数の出力結果の型は必ずしも統一されない。文字列を返してもいいし数値を返してもいい。
となると直和が必要になるなぁ。
:struct @ Media @ Book - NewsPaper
>mediaId : id - string
<
俺は今何を作ってるんだ?わからなくなってきた。
とにかくハイフンでつないでください。「nullable – string」とか話した気がするけどそれみたいな感じだ。例の「mediaId」は直和の「Media」のみに生えるフィールドだ。直和を扱える表計算ソフトはなかなかエロいと思う。
直和のパターンマッチングを考える。
- 例えば「Book」が「Page」の配列である「pages」を持っていて、「Page」は「pageNumber」を持っている。
- 配列の最後の要素をとるビルトイン関数は「getLast」とする。
:functions
>getPages : {
(a @ Media)
# Book -> a.pages |> .getLast |> _.pageNumber
# NewsPaper -> a.pages |> .getLength
# -> null} !"ページ取得"
<
こうだな。
例えばだけど、「pages」がBook固有のフィールドであったとしても大丈夫だね。ちなみに、型名は大文字スタートだし関数名は小文字スタートだよ。
▼集合操作
filterとmapとflattenとfoldだろか。や、サジェストとかセレクトで使うんだから重複除去とかtakeとかskipが必要っすね!
とにかく高階関数(関数を引数にとる関数)でどうにでもなるね。
:functions
>getHoges : {(a @ number[]) => .filter(a, {(x) => (x > 100)})}
>getHoges2 : .filter({(x) => (x > 100)}) !"上と同じ動作"
<
filterに先に関数を渡したい需要があるだろうから
「{(リスト,関数)=>リスト}」と「{(関数,リスト)=>リスト}」
で多相だろな。
マジでこれ実装難度がもう絶望的。俺が実装するんだとしたらね。
//TODO localStorageからの値取得をしてサジェストとか。
//TODO 数値を文字列にしたいユースケースを考える。(クライアントでの表示の問題な気がする。)format関数 数値ゼロ埋め 小数点記号指定 固定小数点 実装するものを絞る。文字列から数値のユースケース(→IDの下四桁が何千番台とかの判定をしたいと思う。)。
◆制約
オブジェクトの項目名は被ってはいけない。
//TODO 以下、疲れたから放置中だよ。頑張って考えてるよ。
:setelem @ Book !"本" >title @ string !"書籍名" >isbn @ key - string !"ISBN-13" >genre @ string !"ジャンル" >price @ number !"価格(円)" >issueDate @ date !"出版日" >isFun @ boolean !"面白いかどうか" >authorId @ Person - id - string !"著者"< :setelem @ Person !"人" >name @ id - string !"著者名" >birthdate @ date: !"誕生日"< :selectRule @ Book.title > >title : [>"はらぺこあおむし",>"はらぺこゴミむし",>"はらぺこクソむし",>"おどりゃクソ森"] >authorId : authors < :remoteSuggestion @ Book >genre : "http://hoge.hoge/api/Book/getSuggestion" < >hogehoge @ string[]:[ >"あいう", >"ふがふが", >"げこげこ",>] :setElem @ KeyValue >key @ id - string >value @ Person[] < >hoges @ KeyValue[]:[>"hoge">[>"hoge">1992-10-07,],] >authors @ Person[]: [ >"田中 芳樹">1952-10-22,>"田中 芳樹2">1952-10-23]
▼セレクトレンジ
◆キーバリュー
◆紐づけレコード作成
参照関係にあるオブジェクトは基本的に同画面で作成できるだろう。仮IDで紐づけて一気に登録とかもできるだろうな。
◆型と列挙
曜日と時間も必要だなーと思った。例えば「毎週月曜の午後七時」とかを入力するアレがあるな。曜日に標準時とか付けちゃったりして。いや、ファイル自体に標準時を付けるメタフィールドが必要だな。
幸いにして文字列はダブルコーテーションでくくられているんで、どうにでも標準型を定義できる。これらは直和っちゃ直和なんだけど特別扱いしてたもれ。
- Null「null」
- 真偽「true/false」
- 性別「male/female」
- 曜日「mon/tue/wed/thu/fri/sat/sun」
- 色「red/blue/yellow・・・」
⇒Web標準色 - 年/月/時間
- バージョン「x.x.x」
⇒たぶんrcとかあるよねー
これらは別に予約語でもないんで、別にプロパティ名がnullでもよかろう…
え?
いや待て、いいのかそれ。trueやらfalseやらnullってプロパティ名のデータをシステムに食わせようとしたら実装壊れるだろ。あと、型バリデーションのところはプロパティ名だけで項目を引き出せるから齟齬が発生するね。
はて…(困惑)
とりあえず、型バリデーションのところは「[型名].[プロパティ名]」で参照するようにする。した。これで「true」って名前のプロパティを定義しても大丈夫。
で、えーと…(狼狽)
プロパティ名を文字列として渡し、内容を取得できるメソッドをライブラリに実装することを要請しよう。そうしよう。
で、特定の言語で使われている予約語でプロパティ名を定義しようとしたときに、エディタがアラートを出すようにしろ。そうしろ。
いいんだろかそれで。
◆エラーハンドリング
関数のエラーハンドリング機構が必要になってしまった。やっぱり関数作ったら面倒なことになった。
で、「例外」とかじゃなくて「処理あきらめ値」みたいなのを戻り値として返せばいいんじゃないかと思った。
引数に「あきらめ値」が入った関数は、処理に入る前に有無を言わさず「あきらめ値」を返す。すべては関数で繋がっているわけだから、「あきらめ値」だった時に値を代用する仕組みさえあればいいと思うんだ。だから、加えて「あきらめ値」しか受け付けない関数の仕組みがありゃいいかな。「あきらめ値」以外はすべて貰った引数を返す。あきらめフィルター。
◆//TODO パーミッション?
◆書式(通知)
条件付き書式を定義する。つまり、「値が100を超えたら赤」とか「値がマイナス値なら注釈をつける」とか。
バリデーションだとエラーになっちまうから、「エラーにはならないけど何かお知らせする」ために書式が必要だ。
まずスタイルの定義。
:styling >hoge: >background : red >fontBold : true >fontColor : blue >border : yellow >link : "http://hoge.hoge/foo?id=" <
//TODO スタイルが当たる条件
◆情報取得リンク
//TODO 情報を参照するためのリンク
◆不変ルール
//TODO 条件付きのデータ固定
◆テンプレートリテラル
いいかげん凄いことになってきたな。壮大すぎる。
テンプレートリテラルは
`"TEST"const.hogehoge"TEST"`
だろうな。バッククォートで囲む。
これなら同じようにminify可能だ。ケツに文字列がつかなかった場合は
`"TEST"const.hogehoge`
的に書いてくれ。
文字列使ってないんなら囲う必要はない。数値とか日付を文字列として扱いたい時は囲っていいよ。
文字列へのフォーマットの定義はどうしようか。:formatでやるか!うわやべぇ!このデータ構造無敵じゃん!こわ!
◆フォーマット
//TODO 鋭意考え中
◆インポート
正規表現のインポートとかしたいね。
:import >hugahuga @ regex : http://hoge.hoge/api/getHugahuga >hogehoge @ validate : http://hoge.hoge/api/getHogehoge <
こんなん。使い方は「/regex.hugahuga」でいいよ。
おなじURLで複数のregexが取れたりする場合は、名前に対応する正規表現しかとれない。
//TODO シノニムの指定
◆コメント
必要性あまり感じないけど任意の場所に「//””」でいいんじゃない?知らね。
空いてる場所ならどこにいれてもいいよ。読まねぇから。
◆NULL文字
考えてなかった。が、データ内にNULL文字を入れることは許さん。
blobを送るための仕組みが必要っすね。上位レイヤーな話、つまりプロトコルの決めの問題だと思うから急がなくても良かろう。
いつか考えます。
あと、なんかハッシュ関数がNULL文字吐きやがるとかって記事を見たんだけど、16進表記してどうぞ。
◆このデータ記法の目指すところについて
- データを、軽量かつ安全かつ柔軟にやり取りする。
- IDEやエディタからのサポートを十分に受けることが出来る。
- ExcelやGoogle Spred的なツールに食わせても安心安全好評絶賛。
- 受け取ったデータをもとにクライアント側で入力フォームを出すことができる。
◆名前
関わるものすべてに対して省エネな記法なので「sho ene notation」で。ひとに説明するときは「save energy notation」って感じで諦めてもいいよ。
拡張子は「.sen」でよかろう。硬くて手ごろで投げるものだから「stone」って名前をつけたかったけど、stoneの検索結果を汚さないようにした。
「せん」だから漢字で書くと「巛」だな。マークはそれでよかろう。
◆データのストリーム読み込み対応
CSVとか行の読み出しで処理出来て幸せ。メモリを大量に確保しなくてすんで幸せ。だから、データのみを1ファイルに格納する方法を提供する。
「ぴよぴよ.sen」と、同階層に「ぴよぴよ.hoge.sen」みたいな。通信するときはソレらをアーカイブ(zipとかgzip)に固めてとばせ。「ぴよぴよ.sen」からは相対パスで「ぴよぴよ.hoge.sen」を読み込むことが出来る。
仕様は後で考える。
◆ほか
2進16進のうけつけ。パーサーの為の日付プレフィクス。
コメントを残す