1 Punkte von GN⁺ 4 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • Wenn man in Datenstrukturen Einträge mit Kommas trennt, sorgt die Zulassung von nachgestellten Trennzeichen dafür, dass Hinzufügen, Löschen und Umordnen von Einträgen als dieselbe Art von Textänderung behandelt werden kann
  • JSON verbietet ein Komma nach dem letzten Member, wodurch beim Hinzufügen oder Löschen eines Schlüssels am Ende ein Sonderfall entsteht, bei dem auch die bestehende letzte Zeile geändert werden muss
  • Haskell-Records, TLA+-Variablendeklarationen und Prolog-Regeln werden wegen der Position von Trennzeichen oder Abschlusszeichen ebenfalls so behandelt, dass Änderungen an der ersten bzw. letzten Zeile unterschiedlich ausfallen
  • Python und Go erlauben nachgestellte Kommas, aber keine vorangestellten; Alloy erlaubt sowohl vorangestellte als auch nachgestellte Kommas
  • Nachgestellte Trennzeichen können in Kontrollstrukturen zu Parsing-Mehrdeutigkeiten führen und werden selbst in Datensyntax, etwa bei Python-Tupeln mit einem Element, zur Bedeutungsunterscheidung verwendet

Das Problem mit dem letzten Komma in JSON

  • In JSON-Objekten sind Kommas zwischen Membern erlaubt, ein Komma nach dem letzten Member ist grammatikalisch jedoch nicht zulässig
{
    "a": 1,
    "b": 2,
    "c": 3
}
  • Fügt man im selben Objekt wie bei "c": 3, ein Komma nach dem letzten Member hinzu, wird es zu ungültigem JSON
{
    "a": 1,
    "b": 2,
    "c": 3,
}
  • Wenn nachgestellte Kommas erlaubt wären, müsste man beim Hinzufügen von "x" vor "a" und von "y" nach "c" nur Zeilen derselben Form ergänzen
{
+   "x": 0,
    "a": 1,
    "b": 2,
    "c": 3,
+   "y": 4,
}
  • In der aktuellen JSON-Grammatik muss man beim Hinzufügen eines Schlüssels an letzter Position auch an die bisher letzte Zeile "c": 3 ein Komma anhängen, wodurch die Änderung komplizierter wird
{
+   "x": 0,
    "a": 1,
    "b": 2,
-   "c": 3
+   "c": 3,
+   "y": 4
}
  • Auch beim Entfernen von Elementen reicht es nicht, nur die betreffende Zeile zu löschen; man muss zusätzlich prüfen, dass am Ende kein nachgestelltes Komma übrig bleibt
  • Wenn der Objektwert selbst ein mehrzeiliges Array oder Objekt ist, werden Umformungen durch das Verbot „kein nachgestelltes Komma“ noch komplizierter

Ähnliche Fälle in anderen Sprachen

  • Haskell-Records

    • Haskell erlaubt bei Record-Typen einen Stil mit „teilweisen Bullet Points“, bei dem das Komma am Anfang jeder Zeile steht
    data Drone = Drone
      { xPos :: Int
      , yPos :: Int
      , zPos :: Int
      }
    
    • Das erleichtert Änderungen an der letzten Zeile, macht Änderungen an der ersten Zeile aber schwieriger
  • TLA+

    • In TLA+ ist bei Variablenlisten und Sequenzen die Form ohne abschließendes Komma gültig
    VARIABLES a, b, c
    vars ==
    
    • Im selben Konstrukt ist ein Komma nach dem letzten Eintrag ungültig
    VARIABLES a, b, c,
    vars ==
    
    • Beim Schreiben von TLA+-Spezifikationen fügt man laufend neue Top-Level-Variablen hinzu, weshalb diese Einschränkung unpraktisch ist
    • Im PlusCal-DSL gibt es dasselbe Problem nicht; Variablendeklarationen können dort mit Semikolons aufgelistet werden
    (*--algorithm foo {
    variables a; b; c;
    
  • Prolog

    • Logiksprachen wie Prolog erlauben nicht nur keine nachgestellten Trennzeichen, sondern verwenden zusätzlich ein eigenes Abschlusszeichen
    foo(A, B, C) :-
        A = 1, % comma
        B = 2, % comma
        C = 3. % period!
    
    • Man kann den abschließenden Punkt auf eine eigene Zeile setzen und damit ein wenig wie eine schließende Klammer behandeln, aber das ist keine Standardsyntax und liefert dennoch keine nachgestellten Trennzeichen
    foo(A, B, C) :-
        A = 1,
        B = 2,
        C = 3
    .
    

Ein besserer Ansatz

  • Sprachen, die nachgestellte Trennzeichen erlauben

    • Go erlaubt in Map-Literalen ein Komma nach dem letzten Eintrag
    valid := map[string]int{
            "a": 1,
            "b": 2,
            "c": 3,
        }
    
    • Python erlaubt in Dictionaries ebenfalls ein Komma nach dem letzten Eintrag
    valid = {
      "a": 1,
      "b": 2,
      "c": 3,
    }
    
    • In Python und Go dürfen Kommas hinten stehen, aber nicht vorne, daher ist ein vollständiger Bullet-Point-Stil nicht möglich
    invalid = {
        , "a": 1
        , "b": 2
        , "c": 3
    }
    
  • Vorangestellte Trennzeichen und Alloy

    • TLA+ erlaubt vorangestellte Konjunktionen und vorangestellte Disjunktionen, aber keine nachgestellte Form wie (a &&)
    // Not TLA+ but the same semantics
    || && a == 1
       && b == 2
    
    || && a == 3
       && b == 4
    
    • Alloy erlaubt sowohl vorangestellte als auch nachgestellte Kommas
    sig Valid {
        , a: 1
        , b: 2
    }
    
    sig AlsoValid {
        a: 1,
        b: 2,
    }
    
    • Alloy erlaubt sogar leere Trennzeichen, sodass auch Zeilen mit nur mehreren Kommas gültig sind
    sig StillValid {
        ,, a: 1,,
        ,,,,,,,,,
        ,, b: 2,,
    }
    
    • Diese Form wird von manchen Leuten als „stuttering“ bezeichnet

Gegenargument: Parsing-Mehrdeutigkeit

  • Prologs Steuertrennzeichen

    • Ein Argument gegen nachgestellte Trennzeichen ist, dass das Parsing mehrdeutig werden kann
    • Wenn in Prolog eine Regel mit einem Punkt endet, ist klar, dass foo und bar getrennte Definitionen sind
    foo(A, B) :-
        A = 1,
        B = 2.
    
    bar(c).
    
    • Ersetzt man das Regelabschlusszeichen durch ein Komma, könnte bar(c) auch als Teil der foo-Definition interpretiert werden
    foo(A, B) :-
        A = 1,
        B = 2,
    
    bar(c),
    
    • In diesem Fall könnte foo so interpretiert werden, dass es nur dann wahr ist, wenn auch bar(c) wahr ist
  • Rubys Methodenaufrufe

    • In Ruby kann man Methodenketten auch nach einem Zeilenumbruch fortsetzen; der folgende Code gibt 5 aus
    puts 3.
         succ().
         succ()
    
    • Würde man nach Methodenaufrufen nachgestellte Trennzeichen erlauben, wäre nicht mehr klar, ob quux() eine Top-Level-Funktion oder eine Methode von foo ist
    foo.
      bar().
      baz().
    
    quux()
    
    • Die Beispiele aus Prolog und Ruby betreffen Mehrdeutigkeiten bei Steuertrennzeichen, nicht bei Datentrennzeichen

Ausnahme in der Datensyntax: Python-Tupel

  • Python verwendet Klammern sowohl zum Gruppieren von Ausdrücken als auch zur Definition von Tupeln
  • (2+3) wird als Auswertung eines Ausdrucks behandelt und ergibt einen int
>>> x = (2+3)
>>> type(x)

  • (2+3,) wird wegen des nachgestellten Kommas als Tupel mit einem Element behandelt
>>> x = (2+3,)
>>> type(x)

  • In diesem Python-Fall dient das nachgestellte Datentrennzeichen dazu, zwischen einem Ausdruck und einem Tupel mit einem Element zu unterscheiden

1 Kommentare

 
GN⁺ 4 시간 전
Lobste.rs-Kommentare
  • Die JSON-Syntax erlaubt Kommata zwischen zwei Mitgliedern eines Objekts, aber keine Trailing Commas nach einem Mitglied. Ich denke nicht, dass man das einen „Designfehler“ nennen kann. Es war damals schlicht keine Option JSON entstand etwa 2000–2001 als Teilmenge von ECMAScript 3, und das informative RFC 4627 wurde 2006 geschrieben. Dass es eine Teilmenge von JavaScript war und sich im Browser direkt mit eval ausführen ließ, war sowohl der Zweck von JSON als auch der Schlüssel zu seinem Erfolg; native JSON-APIs in Browsern kamen erst 2009 hinzu ES5 spezifizierte Trailing Commas ebenfalls erst im Dezember 2009, daher konnte JSON mit Trailing Commas von vornherein nicht existieren, ohne seinem Zweck zu widersprechen

    • Es wirkt so, als würden nur aktive Handlungen als Fehler gelten, aber nichts zu tun ist ebenfalls eine Entscheidung, daher finde ich es legitim, auch das als Fehler zu bezeichnen
  • In Prolog ist das eines der häufigsten Ärgernisse. Wenn man an einem Prädikat arbeitet, ist es praktisch, am Ende ,true. anzuhängen, weil man sich dann beim Umordnen oder Auskommentieren der Zeilen darüber nicht um den abschließenden Punkt kümmern muss

    • Ähnlich verwenden wir im SQL unseres Teams die WHERE-Klausel in der Form where true / and ... / and .... Die Schrägstriche stehen hier für Zeilenumbrüche So kann man beliebige Bedingungen leicht bearbeiten, ohne einen Sonderfall behandeln zu müssen
  • Zig erlaubt Trailing Commas, und man kann damit den nicht konfigurierbaren Formatter steuern .{1, 2, 3,} wird zu

    .{
       1,
       2,
       3,
    }
    

    Und wenn man bei vertikal formatierten Literalen das Trailing Comma weglässt, bedeutet das, dass horizontale Ausrichtung gewünscht ist: .{ 1, 2, 3 } Das funktioniert auch hier: Container-Typdefinitionen struct { a: u32, b: u32, }, Funktionssignaturen fn foo(a: u32, b: u32,) void {}, Funktionsaufrufe foo(1, 2,); In all diesen Fällen kann man mit dem Trailing Comma die automatische Formatierung steuern Diese Funktion gefällt mir so gut, dass ich sie auch meinem HTML-Sprachserver/Auto-Formatter hinzugefügt habe Before:

    Foo
    
    

    After:

    Foo
    
    

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

  • Stimme zu 100 % zu. Eine neue Sprache ohne Trailing Separatoren bekommt bei mir persönlich einen kleinen Minuspunkt. Nicht genug, um ihre Syntax zu hassen, aber so ein kleiner, schmerzhafter Makel

    • Gilt das auch für Funktionsaufrufe? foo(1,2,3,4,)? bar(,1,2,3,4)? Wenn foo() eine variable Anzahl an Parametern erlaubt, ist dann das letzte Argument nil? Ist beim ersten Parameter von bar() dann nil gemeint?
  • In Clojure und EDN sind Kommata Leerraum. Üblicherweise setzt man sie in Map-Literalen auf derselben Zeile konventionell zwischen Schlüssel-Wert-Paare, aber sie sind völlig optional

    {:a 1 :b 2}
    ;=> {:a 1, :b 2}
    {:a,1,,,,,,,,,,:b,2,} ; if you must
    ;=> {:a 1, :b 2}
    
  • Ich denke, ein wesentlicher Grund für die Spannung hier ist, dass man bei mehreren Klauseln in derselben Zeile irgendwie einen Separator braucht

    function(1, 2, 3, 4)
    

    In solchen Fällen wirken Diffs beim Hinzufügen oder Entfernen von Argumenten auf Zeilenbasis immer etwas unbeholfen. Auch wenn man nur ein einzelnes Argument auskommentieren will, braucht das etwas Aufmerksamkeit und Mühe. Das ist der Preis für die kompakte Schreibweise mit mehreren Elementen in einer Zeile Sobald man aber beginnt, pro Zeile nur noch eine Klausel zu setzen, wünscht man sich idealerweise sauberere Diffs und eine einfache Möglichkeit, Dinge mit Zeilenkommentaren zu deaktivieren Die offensichtliche Antwort wäre, Zeilenumbrüche als Standard-Separator zu behandeln

    function(
      1
      2
      3
    )
    

    Viele Sprachen machen das bei ; so. Wenn der Stil davon abrät, mehrere Statements in eine Zeile zu packen, sieht man ; fast nie. Eine Sprache, die das Gleiche mit Kommata macht, kenne ich nicht, aber ich wollte das in einer Hobby-Sprache schon einmal ausprobieren Natürlich umgeht Lisp dieses Problem, weil dort Unterausdrücke und Unterklauseln immer vollständig getrennt sind und es überhaupt keine Separatoren gibt

  • Das ist einer der Gründe, warum ich Lisp, genauer gesagt S-Ausdrücke, mag. Ein Detail weniger, über das man nachdenken muss