設定語言

都 2026 了 AI/Rust 這些新穎的東西都不再新穎了結果設定檔語言還是一坨,這世界上這麼多聰明的人怎麼就沒人設計的出來一個好讀又好編輯的標記語言阿。

說實在以 Docker 或是 Hugo 這種設定我寧願用 JSON 配置,只是 JSON 太老太古板,沒註解,不能逗號,不能多行,沒有 merge 功能,如果有的話我覺得 Docker/Hugo 這種中低複雜度的設定 JSON 反而比 yaml/toml 好多了:yaml/toml 說初衷是設計給人類編輯的,結果寫起來比 JSON 更難理解,比如 yaml 單看 key 本身不知道這個 key 的資料結構

xx:
  - 1
  - 2
  - 3
yy:
  a: 1
  b: 1
zz:
  - a: 1
  - b: 1

等效於這個 JSON

{
  "list": [
    1,
    2,
    3
  ],
  "dict": {
    "a": 1,
    "b": 1
  },
  "list_of_dict": [
    {
      "a": 1
    },
    {
      "b": 1
    }
  ]
}

就很荒謬,只看 key 本身看不出來 key 自己到底是 list 還是 dict。

TOML 更蠢,top level 設定只能放在檔案最上方,蠢到家的設計。TOML 深層字典一定要重寫所有 parent 如 [xxx.a1.b1] [xxx.a1.b2],不然就用不能換行的 inline table 功能,最後又變回 JSON,這到底什麼天才,而且 TOML 相較 JSON/YAML 太新,每個編輯器、每個人對他的 formatter 看法都不一樣,有些要縮有些不縮,相關工具鏈無內建還要自己找超級麻煩。

很會抱怨,那有沒有好建議呢,實際上在 JS 生態都直接用 JS 作為設定檔,真的很好用,但是和這些純靜態設定檔就完全不同場景了,我是希望 PKL/CUE 這兩個 Apple/Google 的設定語言能更廣泛、更多採用。

https://github.com/apple/pkl/discussions/7

結論是我還是挺 JSONC/YAML,TOML 還是滾一邊去吧,完全沒辦法設定複雜的內容,如果只能設定簡單的,那根本就沒和 YAML 拉開優勢。看這個真實的 Hugo 設定檔就知道 TOML 多蠢,根本看不懂。

YAML 一目了然,segments.fast 底下兩個 key。

segments:
  fast:
    excludes:
      - kind: taxonomy
      - path: '{/tags,/tags/**,/categories,/categories/**}'
      - output: 'rss'
      - sites:
          matrix:
            languages:
              - zh-cn
    includes:
      - path: /
      - path: /**

JSON 繁瑣,但是同樣一目了然層級關係

{
  "segments": {
    "fast": {
      "excludes": [
        {
          "kind": "taxonomy"
        },
        {
          "path": "{/tags,/tags/**,/categories,/categories/**}"
        },
        {
          "output": "rss"
        },
        {
          "sites": {
            "matrix": {
              "languages": [
                "zh-cn"
              ]
            }
          }
        }
      ],
      "includes": [
        {
          "path": "/"
        },
        {
          "path": "/**"
        }
      ]
    }
  }
}

接著輪到天兵上場,TOML 不全部看完就不能確定 key 的範圍和層級,而且每個 key 還要重複手寫。

[[segments.fast.excludes]]
  kind = "taxonomy"

[[segments.fast.excludes]]
  path = "{/tags,/tags/**,/categories,/categories/**}"

[[segments.fast.excludes]]
  output = "rss"

[[segments.fast.excludes]]
  [segments.fast.excludes.sites.matrix]
    languages = [ "zh-cn" ]

[[segments.fast.includes]]
  path = "/"

[[segments.fast.includes]]
  path = "/**"

再不然用 TOML inline table 改寫,但是不能換行,一樣難讀:

segments.fast.excludes = [
  { kind = "taxonomy" },
  { path = "{/tags,/tags/**,/categories,/categories/**}" },
  { output = "rss" },
  { sites = { matrix = { languages = [ "zh-cn" ] } } },
]

segments.fast.includes = [
  { path = "/" },
  { path = "/**" },
]

最後就如同我說的,簡單的沒優勢,複雜的更難懂,那 TOML 到底有啥用,天兵語法一個。以下列出更多問題,大概 1/3 是我寫的,另外 2/3 是 Claude 補充的。

JSON

  • 無法寫註解(官方規格禁止)
  • 結尾逗號不合法
  • 所有 key 必須加引號,冗餘且噪音高
  • 沒有多行字串原生支援,換行必須用 \n 轉義
  • 字串中的特殊字元需要大量轉義
  • 不支援十六進位、二進位等數字字面值
  • 不支援無限大(Infinity)與 NaN
  • 大量括號與引號,視覺噪音極高
  • 深層巢狀時縮排與括號數量讓人難以追蹤
  • 無法表達有序的 key
  • 整數與浮點數無明確區分(統一為 number)
  • 無 date/datetime 原生型別
  • 無二進位資料型別
  • 無法引用或重複使用設定片段(無錨點機制)

YAML

  • 縮排敏感,Tab 字元完全禁止
  • 縮排層級沒有明確符號標記,只能靠空白數量辨識
  • 型別推斷過於激進:no、yes、on、off 被轉成布林值(YAML 1.1),2024-01-01 被轉成日期,1_000 被轉成整數
  • 多行字串有 |(保留換行)、>(折疊換行)兩種,加上 |-、>- 等變體,共六種以上,難以記憶
  • 特殊字元如 : 後面接空格就變成 key-value 分隔符
  • {} 與 [] 引入 flow style,等同於在 YAML 裡嵌入 JSON,兩套語法混用
  • 複雜文件可讀性比 JSON 更差,因為沒有明確的結構符號
  • 巢狀物件的起始與結束完全靠縮排,無閉合符號,容易迷失位置
  • 不同編輯器、不同作業系統的空白處理可能不一致
  • 錨點(&)與別名(*)機制雖然存在但鮮為人知且功能有限
  • Merge key(<<)是非標準擴展,並非所有 parser 支援
  • YAML 1.1 與 YAML 1.2 的型別推斷規則不同,相同內容在不同 parser 下行為不一致
  • 多文件(--- 分隔)支援讓 parser 複雜化

TOML

  • [table] 與 [[array of tables]] 的雙括號區分雖然有創意,但初學者極度困惑
  • 深層巢狀需要重複寫出完整路徑,如 [server.host.region.zone],路徑愈深愈冗餘
  • 同一個父層下的多個子表需要重複宣告父路徑,如你提到的 [xxx.a1.b1]、[xxx.a1.b2] 問題
  • inline table({a = 1, b = 2})與一般 table 語法不一致,且規格要求 inline table 必須單行,不可換行
  • inline table 等同於把 JSON 的物件語法引入,繞了一圈回到原點
  • Array 中的物件只能用 [[]] 表達,無法混合使用 inline 與 block 風格
  • key 可以帶引號也可以不帶,裸 key 只允許 A-Za-z0-9_-,規則不統一
  • 設定的順序影響語意(重複定義同一 table 是錯誤),讀者必須記住哪些 table 已被定義
  • 大型設定檔中 [table] 標頭散落各處,難以看出整體層級結構
  • 複雜巢狀的可讀性甚至不如 JSON
  • 不支援 null
#隨筆