1 ポイント 投稿者 GN⁺ 4 시간 전 | 1件のコメント | WhatsAppで共有
  • データ構造で項目をカンマで区切る際、末尾カンマを許可すると、項目の追加・削除・並べ替えを同じ種類のテキスト変更として扱える
  • JSON は最後のメンバーの後ろのカンマを禁止しているため、末尾にキーを追加または削除する際に既存の行まで修正しなければならない特殊ケースが生じる
  • Haskell のレコード、TLA+ の変数宣言、Prolog の規則でも、区切り文字の位置や終端記号のために先頭行の変更と末尾行の変更が異なる扱いになる
  • Python と Go は末尾カンマを許可するが先頭カンマは許可せず、Alloy は先頭・末尾カンマの両方を許可する
  • 末尾区切り文字は制御構文では構文解析の曖昧さを生むことがあり、Python の単一要素タプルのようにデータ構文でも意味の区別に使われる

JSON の最後のカンマ問題

  • JSON オブジェクトではメンバー間のカンマは許可されるが、最後のメンバーの後ろに来るカンマは文法上許可されない
{
    "a": 1,
    "b": 2,
    "c": 3
}
  • 同じオブジェクトで "c": 3, のように最後のメンバーの後ろにカンマを付けると、無効な JSON になる
{
    "a": 1,
    "b": 2,
    "c": 3,
}
  • 末尾カンマが許可されていれば、"a" の前に "x" を追加し、"c" の後ろに "y" を追加する場合でも、同じ形の行追加だけで済む
{
+   "x": 0,
    "a": 1,
    "b": 2,
    "c": 3,
+   "y": 4,
}
  • 現在の JSON 文法では、最後の位置にキーを追加する際に既存の最後の行 "c": 3 にもカンマを付けなければならず、変更がより複雑になる
{
+   "x": 0,
    "a": 1,
    "b": 2,
-   "c": 3
+   "c": 3,
+   "y": 4
}
  • 要素を削除する場合も、その行だけを消せばよいわけではなく、最後の行に末尾カンマが残らないか確認しなければならない
  • オブジェクトの値自体が複数行の配列やオブジェクトである場合、「末尾カンマなし」による変換はさらに複雑になる

他の言語における似た例

  • Haskell レコード

    • Haskell ではレコード型で、各行の先頭にカンマを置く「部分的な箇条書き」スタイルを使える
    data Drone = Drone
      { xPos :: Int
      , yPos :: Int
      , zPos :: Int
      }
    
    • この方式は最後の行を変更しやすくする一方で、最初の行は変更しにくくなる
  • TLA+

    • TLA+ では変数リストやシーケンスで、末尾にカンマがない形は有効である
    VARIABLES a, b, c
    vars ==
    
    • 同じ構文で最後の項目の後ろにカンマを付けると無効になる
    VARIABLES a, b, c,
    vars ==
    
    • TLA+ の仕様を書く際には最上位変数を継続的に追加することが多いため、この制限は不便である
    • PlusCal DSL では同じ問題はなく、変数宣言をセミコロンで並べられる
    (*--algorithm foo {
    variables a; b; c;
    
  • Prolog

    • Prolog のような論理言語は末尾区切り文字を許可しないだけでなく、別の終端記号も使う
    foo(A, B, C) :-
        A = 1, % comma
        B = 2, % comma
        C = 3. % period!
    
    • 最後のピリオドを別行に置く方式なら中括弧のように見なすこともできるが、標準構文ではなく、末尾区切り文字も得られない
    foo(A, B, C) :-
        A = 1,
        B = 2,
        C = 3
    .
    

より良い方法

  • 末尾区切り文字を許可する言語

    • Go はマップリテラルで最後の項目の後ろのカンマを許可する
    valid := map[string]int{
            "a": 1,
            "b": 2,
            "c": 3,
        }
    
    • Python も辞書で最後の項目の後ろのカンマを許可する
    valid = {
      "a": 1,
      "b": 2,
      "c": 3,
    }
    
    • Python と Go のカンマは後ろには置けるが前には置けないため、完全な箇条書きスタイルにはできない
    invalid = {
        , "a": 1
        , "b": 2
        , "c": 3
    }
    
  • 先頭区切り文字と Alloy

    • TLA+ は前置の連言や前置の論理和演算を許可するが、(a &&) のように後ろに付ける方式は許可しない
    // Not TLA+ but the same semantics
    || && a == 1
       && b == 2
    
    || && a == 3
       && b == 4
    
    • Alloy は先頭カンマと末尾カンマの両方を許可する
    sig Valid {
        , a: 1
        , b: 2
    }
    
    sig AlsoValid {
        a: 1,
        b: 2,
    }
    
    • Alloy は空の区切り文字も許可するため、カンマだけが複数並ぶ行も有効として扱われる
    sig StillValid {
        ,, a: 1,,
        ,,,,,,,,,
        ,, b: 2,,
    }
    
    • このような形は一部の人に「stuttering」と呼ばれている

反論: 構文解析の曖昧さ

  • Prolog の制御区切り文字

    • 末尾区切り文字に反対する論拠の一つは、構文解析が曖昧になりうる点である
    • Prolog ではピリオドで規則を終えると、foobar が別々の定義であることが明確になる
    foo(A, B) :-
        A = 1,
        B = 2.
    
    bar(c).
    
    • 規則の終端記号をカンマに置き換えると、bar(c)foo 定義の一部として解釈される可能性がある
    foo(A, B) :-
        A = 1,
        B = 2,
    
    bar(c),
    
    • この場合、foobar(c) も真であるときにのみ真だと解釈されうる
  • Ruby のメソッド呼び出し

    • Ruby では改行後もメソッドチェーンを続けて書け、以下のコードは 5 を出力する
    puts 3.
         succ().
         succ()
    
    • メソッド呼び出しの後に末尾区切り文字を許可すると、quux() がトップレベル関数なのか foo のメソッドなのかが明確でなくなる
    foo.
      bar().
      baz().
    
    quux()
    
    • Prolog と Ruby の例は、データ区切り文字ではなく制御区切り文字に関する曖昧さである

データ構文における例外: Python タプル

  • Python は括弧を式のグループ化とタプル定義の両方に使う
  • (2+3) は式の評価として扱われ、int になる
>>> x = (2+3)
>>> type(x)

  • (2+3,) は末尾カンマによって単一要素タプルとして扱われる
>>> x = (2+3,)
>>> type(x)

  • Python のこの例では、末尾データ区切り文字が式と単一要素タプルを区別する役割を果たしている

1件のコメント

 
GN⁺ 4 시간 전
Lobste.rsの意見
  • JSONの文法では、オブジェクトの2つのメンバーの間にカンマを置くことはできるが、メンバーの後ろに末尾カンマを置くことはできない。これを「設計ミス」と呼ぶのは適切ではないと思う。選択肢ではなかったからだ JSONは2000〜2001年ごろにECMAScript 3の部分集合として作られ、情報提供RFC 4627は2006年に書かれた。JavaScriptの部分集合であるためブラウザでevalでそのまま動くことがJSONの目的であり成功の核心で、ブラウザのネイティブJSON APIが追加されたのは2009年になってからだった ES5で末尾カンマが仕様化されたのも2009年12月なので、末尾カンマ付きJSONはそもそも目的に合わず、存在し得なかった

    • 積極的な行動だけをミスと見るようだが、何もしないことも選択であり、したがってミスと呼ぶのも妥当だと思う
  • Prologで最もよく感じる不便の1つがこれ。述語を作業するとき、最後に,true.を付けておくと、上の行を並べ替えたりコメントアウトしたりするときに最後のピリオドを気にしなくて済むので便利

    • 同様にSQLでも、うちのチームはwhere true / and ... / and ...という形でWHERE句を書く。ここでスラッシュは改行を意味する こうすると、どの条件でも特別扱いせず簡単に編集できる
  • Zigは末尾カンマを許可しており、これを使って設定不能なフォーマッタを制御できる .{1, 2, 3,}は次のように変わる

    .{
       1,
       2,
       3,
    }
    

    そして縦に整形されたリテラルで末尾カンマを外すと、横並びを要求する意味になる: .{ 1, 2, 3 } 次でも機能する: コンテナ型定義 struct { a: u32, b: u32, }、関数シグネチャ fn foo(a: u32, b: u32,) void {}、関数呼び出し foo(1, 2,); こうしたすべてのケースで、末尾カンマで自動整形を制御できる この機能があまりに気に入ったので、自分のHTML言語サーバー/自動フォーマッタにも追加した Before:

    Foo
    
    

    After:

    Foo
    
    

    https://github.com/kristoff-it/superhtml

  • 100%同意。末尾区切り文字がない新しい言語は、個人的には少し減点対象。言語の文法が嫌いになるほどではないが、小さな傷のような不便さがある

    • 関数呼び出しでもそうなのか? foo(1,2,3,4,)bar(,1,2,3,4)foo()が可変個引数を許すなら最後の引数はnilなのか? bar()の最初の引数はnilなのか?
  • ClojureとEDNではカンマは空白。通常は同じ行のマップリテラルでキーと値のペアの間に慣習的に使うが、完全に任意だ

    {:a 1 :b 2}
    ;=> {:a 1, :b 2}
    {:a,1,,,,,,,,,,:b,2,} ; if you must
    ;=> {:a 1, :b 2}
    
  • ここで緊張が生まれる大きな理由は、同じ行に複数の節があると、何らかの区切り文字が必要になるからだと思う

    function(1, 2, 3, 4)
    

    こういう場合、行単位の差分で引数を追加したり削除したりすると常にぎこちなく見える。引数を1つだけコメントアウトするときにも少し注意と手間が必要になる。その代わり、1行に複数の項目を入れる簡潔さを受け入れているわけだ しかし1行に1つの節だけを置くようになると、理想的にはもっときれいな差分と、単純な行コメントで簡単に無効化できる方法が欲しくなる 明白な答えは、改行を標準の区切り文字として扱うことだ

    function(
      1
      2
      3
    )
    

    多くの言語は;についてこれをやっている。1行に複数の文を入れないよう推奨するスタイルなら、;を見ることはほとんどなくなる。カンマにも同じことをする言語はあまり知らないが、趣味の言語で試してみたいと思ったことはある もちろんLispは、下位式と下位節が常に完全に分離されているので、区切り文字自体がなくこの問題を回避している

  • Lisp、正確にはS式を好きな理由の1つがこれ。考えるべき細部が1つ減る