Chienomi

新しいデータ記述言語 TOML

プログラミング::topic

TOMLは新しいデータ記述言語であり、JSONやYAMLなどに対抗する存在である。 特にRustやGoで多様されているらしい。

新しいために不安定な存在だったが、最近1.0.0がリリースされ仕様はfixされた。

RubyではTOMLパーサは色々と存在しているが、本記事ではperfect_tomlを使う。

本記事ではこのデータ記述言語、TOMLを見ていく。

overview

TOMLは「構造化しやすいini」みたいな構造をしている。 こんな感じ。

[user]
name = "Haruka"
nick = "Harukamy"

[account]
email = "haruka@example.com"
hoge = {n = 1000, t = 16:35:00}

  [account.info]
  xtime = 1111111111

すごくiniっぽいが、ドット区切りで名前空間が作れる。 配列, 連想配列, TZつきDateTime, DateTime, Date, Timeをサポートするのがえらい。

例の通り記述としてはインデントすることができるが、基本的にはフラットな構造をベースとするようなものになっている。 上記のTOMLをPerfectTOMLでロードして吐くと次のようになる。

[user]
name = "Haruka"
nick = "Harukamy"

[account]
email = "haruka@example.com"

[account.hoge]
n = 1000
t = 16:35:00

[account.info]
xtime = 1111111111

基本的には設定ファイル向き、人間が書くもの向きの構造だ。 コンピュータ的に見ると吐きにくいし、サイズも小さくないのであまり良いフォーマットだとは言えない。 その意味ではYAMLに近い。

vs JSON

{"user":{"name":"Haruka","nick":"Harukamy"},"account":{"email":"haruka@example.com","hoge":{"n":1000,"t":"16:35:00"},"info":{"xtime":1111111111}}}

JSONは非常に普及しているが、あまりよくないフォーマットだ。

フォーマットは非常に厳格であり、取り扱う型も少ない。そのためにセキュアであり、交換性も高い。 だが、これは現実としては机上の空論となっている。 例えばChrome及びにNode.js搭載されるJSON.parse及びJSON.stringifyDateオブジェクトを文字列に変換し、このDate文字列をDateオブジェクトに変換する。しかし、ECMA-404を読んでもそのような仕様は発見できない。

ECMA-262(JSONではなくECMAScriptの仕様)に文字列との変換の定義がなされているが、JSONのフォーマットではなく、JSONを通じてDateを交換するのは「勝手な拡張」であると言える。

原則を守らずに拡張や変更がなされるのはJavaScriptの悪い文化だが、これはフォーマットとしての交換性が保たれていないので、JSONはセキュアでなく、交換性も低い、となる。

スペースの解釈が大変にフリーダムだが、これはJavaScript処理系の仕様に合わせたものになっている。

当初は機能が少なくライブラリも使いにくいJavaScript上でevalで処理できるという美点があったが、今やそれは許容されていないので、そうした理由も、もはやない。 JSONを使う理由は、JSONが使われてきたから、という以上にはないだろう。

ではそれと比べるとTOMLはどうか。

JSONはJavaScriptというプログラミング言語の事情に由来しているために、人間が書くのには適していないフォーマットである。 キー側のクォートが必須であることや、余計なカンマが許容されないことなど、JavaScriptとして許容できるはずのことを許容しない厳密なフォーマットであることも人間に優しくない。

TOMLは人間が書くことを想定したものであり、書くのも読むのも易しい。 JSONで書かれた設定ファイルは悪い文明だ。

JSONとの比較では、コンピュータ寄りか、人間寄りか、という点で大きな違いが出る。 また、TOMLはDateTimeがちゃんとフォーマットとしてサポートされている点も優れている。

vs YAML

---
user:
  name: Haruka
  nick: Harukamy
account:
  email: haruka@example.com
  hoge:
    n: 1000
    t: '16:35:00'
  info:
    xtime: 1111111111

YMALは非常に強力なフォーマットだ。

基本型としてはDateTimeがサポートされる以外には目立ったところはない。 その書き方に関しては、インデントにより構造を表現できるほか、特に必要がなければ文字列をクォートで囲まなくて良いのも美点だ。 人間が非常に書きやすい。

そして複雑なフォーマットを書くための機能も取り揃えている。

だが、この機能こそがYAMLの闇だ。

コメントが書けるのは良いことだ。それは良いとしよう。 一つのデータの中に複数のYAMLを書くことができるのも、場合によっては便利だ。それも良しとしよう。 実際、そのおかげでYAML Frontmatterなんてものが成立するのだし。 (複数書かなくても終端にはなるため)

だが、「キーはなんでも良い」は危険だ。 その「なんでも良いキー」を書くためのフォーマットも用意されているが、本当に何でも許される。 Rubyはなんでもキーにできるが、なんでもキーにすることは推奨されない。

? - Detroit Tigers
  - Chicago cubs
: - 2001-07-23

? [ New York Yankees,
    Atlanta Braves ]
: [ 2001-07-02, 2001-08-12,
    2001-08-14 ]

タグもなかなか恐ろしい。 タグはそれ自体が名前空間を持っている。この考え方はXMLの名前空間に似ている。 デフォルトのタグ名前空間yaml.org,2002にはYAMLが認識する型が定義されているが、カスタム型の定義をしても良い。 RubyなPsyncはこれを使って任意のオブジェクトをdumpするようになっており、YAMLで定義されていない(つまり交換性のない)フォーマットを吐く。 実際、上記TOMLをYAMLにすると

---
user:
  name: Haruka
  nick: Harukamy
account:
  email: haruka@example.com
  hoge:
    n: 1000
    t: !ruby/object:PerfectTOML::LocalTime
      hour: 16
      min: 35
      sec: 0
  info:
    xtime: 1111111111

となる。 あまり知られていないが、YAMLにリテラルでかけないがタグを使えば書ける型としてomap, set, pairsが用意されている。 mapは連想配列、seqは配列で、omapがOrdered map、setは集合、pairsは2要素配列となる。

標準でこれだけの型があるのはなかなか厄介だ。YAMLにある型のすべてが言語の機能として表現できる言語というのは多くはない。 Rubyは相当、組み込み型の大きい言語だが、連想配列(ハッシュ)の順序に関しては区別がない(以前はunorderedで、今はordered)。 Setも標準ライブラリではあるが、組み込みクラスではない。

便利なようで、もはや考え直したほうがいいレベルに到達するのがアンカーとエイリアスだ。 こんな感じ。

---
foo: &foo
  name: FOOOOO
  power: 10000
bar: &bar
  name: BARRRR
  power: 20000
members:
  - *foo
  - *bar

名前をつけてあとから参照できる。ただ、定義だけできるわけではないので、その値が初出するところにアンカーを打つ必要がある。 さらに、展開することもできる。

---
foo: &foo
  name: FOOOOO
  power: 10000
bar: &bar
  name: BARRRR
  power: 20000
members:
  - *foo
  - <<: *bar
    power: 25000

JavaScriptで書くとこんな感じ。

const foo = {
  name: "FOOOOO"
  power: 10000
}
const bar = {
  name: "BARRRR"
  power: 20000
}
const members = [
  foo,
  {...bar, power: 25000}
]

データ表現としてリファレンスはリファレンスであるべき、という面は、全くないわけではない。 事実、Node.jsにおいてJSON.stringifyは循環構造を持つオブジェクトのシリアライズで例外を吐くが、YAMLにおいてならリファレンスを使って循環構造を表現することが可能だ。

もちろん、非常に強力なフォーマットであることと、ユーザーフレンドリーなフォーマットであることは両立しうるものだ。 Pandoc Markdownだって非常に強力なフォーマットだが、Markdownのユーザーフレンドリーなシンプルさを失っているわけではない。

だが、YAMLはユーザーフレンドリーな、非常に書きやすいフォーマットであるということとは裏腹に、残念ながらユーザーがその複雑さから逃れられない面も持っている。 YAMLを書くことに長けた人が、読みやすい丁寧に書いたYAMLを読むのは容易い。また、それほどYAMLに詳しくない人が、自分で書いたYAMLを読むのも容易い。しかし、YAMLで書くべきものを埋めるために書かれたYAMLは、もはや人間が読み書きするようなものではない。

例えばKubernetesのように、単にYAMLであるだけでなくさらに複雑なSchemeまで乗っているようなものになると、YAMLを苦痛なものへと変えてしまう。だから、あくまでシンプルであり、シンプルなままになるものを求めるのは無理からぬことだ。

TOMLはYAMLほど複雑ではなく、シンプルさが保たれる。 自分が書くデータがシンプルかどうかだけではなく、フォーマットがシンプルかどうかまで気にする人には適しているだろう。

交換性に関しても、カスタムオブジェクトなどは使えないためちゃんと確保されている。 ただ、Time型は多くの言語には存在しないものなので、扱いは少し難しい。

筋よくないかも

プログラミング言語やメタフォーマットなどを作ってみるとわかるのだが、「最初よく考えていなかった要素は辻褄を併せても複雑怪奇なものになる」という現象が発生する。 Perlも、後から追加されたオブジェクト指向は、最初に含めて綺麗に設計されていないのがありありと出ている。

TOMLがこうした「後先よく考えてなかった」という部分を見せるのが、“Array of Tables”だ。 これは、「ひと目見てわかる」とは到底言えない。

[[fruits]]
name = "apple"

[fruits.physical]  # subtable
color = "red"
shape = "round"

[[fruits.varieties]]  # nested array of tables
name = "red delicious"

[[fruits.varieties]]
name = "granny smith"


[[fruits]]
name = "banana"

[[fruits.varieties]]
name = "plantain"

JSONでいうとこうなるのだが

{
  "fruits": [
    {
      "name": "apple",
      "physical": {
        "color": "red",
        "shape": "round"
      },
      "varieties": [
        { "name": "red delicious" },
        { "name": "granny smith" }
      ]
    },
    {
      "name": "banana",
      "varieties": [
        { "name": "plantain" }
      ]
    }
  ]
}

「できる」のと「やりたいことが適切にできる」のは違う。 TOMLのコンセプトに従ってより馴染む形にしたいのであればこれが理想ではないはずだ。

ネストが深くなると直感的に理解するのが困難な記述になっていき、可読性が著しく落ちる。

また、プログラムから吐いた場合にはインデントが保たれないため、ユーザーが書いたものをプログラムがアップデートするというような使い方は難しい。

大概、このような「筋の良くないもの」というのは、流行ったとしてもいまひとつなものになる。 例えば、XMLのように。

このため、TOMLがあまり良いものだとは私は思えない。

実際の使いどころ

私のプログラムで実際に使っているところは次のような感じである。

YAML:

  • PureBuilder Simplyの設定ファイル
  • PureBuilder SimplyのFrontmatter dump
  • PureBuilder Simplyの記事データベース (optional)
  • Multimachines Utilsの設定ファイル
  • My Browser Profile Chooserの設定ファイル
  • Atnowの設定ファイル

JSON:

  • PureBuilder Simplyの交換ファイル
  • PureBuilder Simplyの記事データベース (optional)

この中で、PureBuilder Simplyの設定ファイルに関しては、TOMLにしても良いかもしれない。 例えば、今のChienomiの設定はこんなものだが

outdir: ../Build
toc: yes
post_eruby: no
pandoc_additional_options:
  shift-heading-level-by: 1
testserver_port: 20088
self_url_external_prefix: "https://chienomi.org/"
blessmethod_accs_rel: timestamp

TOMLにするとこうなる。

outdir = "../Build"
toc = true
post_eruby = false
testserver_port = 20088
self_url_external_prefix = "https://chienomi.org/"
blessmethod_accs_rel = "timestamp"

[pandoc_additional_options]
shift-heading-level-by = 1

ただ、「してもいい」だけで、するだけの魅力があるとは思えない。

My Browser Profile ChooserはYAMLは非常にコンパクトに書けるため、TOMLにするのはちょっと厳しい。

amazon: {type: fx}
youtube: {type: chi}
otr:
  type: viv
  opts:
    - --incognito

TOMLで書くと見やすいかもしれないが、書くのがだるい。

[amazon]
type = "fx"

[youtube]
type = "chi"

[otr]
type = "viv"
opts = ["--incognito"]

唯一、アカウント管理に関してはTOMLが良いかなと思う。

アカウント管理はもともと私はiniでやっていて、ルールが不明瞭だったので最近はYAMLにしている。 たが、若干YAMLが合っていない印象も持っているので、TOMLは丁度いいかもしれない。

id:
  username: haruka
  password: PASSW0RD
infomation:
  addr: 地球
  email: haruka@example.com
  payment: card/amex

これよりはこっちがいい。

[id]
username = "haruka"
password = "PASSW0RD"

[infomation]
addr = "地球"
email = "haruka@example.com"
payment = "card/amex"

ただ、現状「手でコピペする」運用なので、クォートが邪魔。