TOMLパーサーを書いた/設定ファイルについて思うこと

TOMLについて

プログラムの設定ファイル、あるいはプロジェクトファイルとしては、さまざまなファイル形式が使われてきました。古くはINI、近年はJSONやYAMLなどです。最近よく見かけるのがTOMLです。Rustの Cargo.toml やPythonの pyproject.toml などで使われています。仕様は

で参照できます。TOMLはTom’s Obvious, Minimal Languageの略ということになっており、ミニマルでわかりやすいことがウリのようです。

私がStandard MLで書くプログラムの設定ファイルにもTOMLを採用するかもしれません。しかし、Standard ML向けの既存のTOMLパーサーはまだなさそうです。なので、実装してみました。

TOMLのデータ型は、JSONに日付・時刻を加えたような感じです。Standard MLで定義すると次のようになるでしょうか:

datatype value = STRING of string (* UTF-8 encoded *)
               | INTEGER of IntInf.int
               | FLOAT of real (* nan と inf も書ける *)
               | BOOL of bool
               | DATETIME of string (* 2024-01-12T19:20:21[.123]+09:00 *)
               | LOCAL_DATETIME of string (* 2024-01-12T19:20:21[.123] *)
               | DATE of string (* 2024-01-12 *)
               | TIME of string (* 19:20:21[.99999] *)
               | ARRAY of value list
               | TABLE of table
withtype table = (string * value) list

設定ファイル全体としては table を表します。

ところで、「JSONみたいなファイル形式が何を表すか」みたいな話は以下の記事にまとまっています:

閑話休題。

自然言語で書かれた仕様は曖昧なので、実際にパーサーを書く上ではテストスイートを使うのが良いでしょう。公式のテストスイートは

にあります。

何点かハマったポイントを紹介します。まず、複数行文字列はダブルクォート3つまたはシングルクォート3つで開始・終了するのですが、終了直前に2つまでのクォートをエスケープせずに書くことができます。つまり、

foo = """foo"""""

foo"" という文字列を表します。

次に、テーブルの内容は複数に分けて書くことができます。

a.b.c = 123
a.b.d = 456

と書くと a = {b = {c = 123, d = 456}} と等価となります。何が問題かというと、「複数に分けて書いたものがマージされる」のか「重複エラーが出る」のルールが微妙なのです。まず、= で値として定義したテーブルを後からヘッダー([] で囲うやつ)として定義できないのは良いでしょう。

a = {}

[a] # エラー
b = 123

しかし、ネストしたキーを持つヘッダーの後に親となるヘッダーを定義することはできます:

# [x] you
# [x.y] don't
# [x.y.z] need these
[x.y.z.w] # for this to work

[x] # defining a super-table afterward is ok

一方、先に = で暗黙に作られたキーはヘッダーとして定義することはできません:

x.y.z.w = {}
[x] # エラー

こういうのを見ると「お前本当にObviousか?」と思ってしまいます。文法はABNFで形式的に書かれていますが、意味論も形式的に書かれたものが欲しかったです。

設定ファイルについて思うこと

最初に書いたように、設定ファイルやプロジェクトファイルの形式としてはこれまでもさまざまなものが登場してきましたし、これからも色々登場するでしょう。私がぱっと思いついたものをいくつか挙げておきます:

  • INI
  • XML
  • JSON
  • YAML
  • TOML
  • S式
    • 例:dune (OCaml)
  • Java properties file

既存のスクリプト言語を使うやつもあります。例を挙げます:

  • JavaScript
    • webpack
  • Python
    • SCons
  • Ruby
    • Homebrew formula
  • Perl
    • latexmk
  • Tcl
    • MacPorts

既存のスクリプト言語を使う場合のメリットは、条件分岐や関数などのロジックを自然に書けることでしょう。YAMLの上にロジックを書こうとする代物は色々ありますが、それぞれ独自言語のようになってはいないでしょうか。

一方で既存のスクリプト言語を使う場合のデメリットは、スクリプト言語という(比較的)変化の激しいものに依存してしまうことです。JavaScriptくらい標準化されていたり、Perlくらい枯れていればまだ良かったりするのかもしれませんが……いやJavaScriptもモジュールシステムで色々あったな……。

もちろん、独自の形式や、独自のスクリプト言語を使う選択肢もあります。Haskellのcabalファイルとか、Emacs Lispとかですね。これのデメリットはシンタックスハイライトとかフォーマッターを専用に用意しないといけないことです。

話を戻します。設定ファイル向けの、ある程度構造化されたものを記述できるモダンな形式としては、JSON、YAML、TOMLが3強でしょう(主観)。それぞれメリット・デメリットを挙げてみます。

  • JSON
    • メリット:シンプル。
    • デメリット:コメントを書けない。ケツカンマが許容されない。
  • YAML
    • メリット:インデントとかを駆使してネストされた構造を書きやすい。文字列のクォートが必ずしも必要ない。
    • デメリット:複雑すぎる。
  • TOML
    • メリット:シンプル。
    • デメリット:「テーブルに入った配列に入ったテーブル」のような複雑な構造は書きづらい(冗長になる)。

TOMLはシンプルで、私の好みに合いそうですが、採用する前に批判の声にも耳を傾けておきたいです。「why not toml」で検索して出てきた記事を挙げておきます:

まあ、簡単な内容ならTOMLでいいけど、(schemaが要るような)複雑な内容になってくるとYAMLの方が有利になってくる、という感じでしょうか。

でもYAMLのパーサーは自分で書きたくありません(私にとっては悪いことに、Standard MLで書かれたYAMLパーサーはまだ存在しません)。YAMLの扱いやすいサブセットがあると良いのですが……というかWhat is wrong with TOMLの記事はStrictYAMLというYAMLのサブセットを作っている人の記事ですが。

こうして人々の欲望は「コメントの書けるJSON」や「YAMLの扱いやすいサブセット」を生み出し、設定ファイルフォーマット界は明日も混沌としているのでした。