Nontrailing separators do not spark joy

Nontrailing separators do not spark joy

非尾随分隔符令人不悦

June 10, 2026 2026年6月10日

Lookin’ at you, JSON. This is valid JSON: 盯着你呢,JSON。这是合法的 JSON:

{ "a": 1, "b": 2, "c": 3 }

This is invalid JSON: 这是非法的 JSON:

{ "a": 1, "b": 2, "c": 3, }

The difference is the last comma. The JSON grammar specifies that a comma can separate two members of an object but not postcede (“trail”) a member. I think this was a design mistake. 区别就在于最后一个逗号。JSON 语法规定逗号可以分隔对象的两个成员,但不能置于成员之后(即“尾随”)。我认为这是一个设计错误。

Say we want to add two new keys to the struct, one before the “a” member and one after the “c” member. Here’s what it would look like if trailing commas were permitted: 假设我们想在结构体中添加两个新键,一个在 “a” 成员之前,一个在 “c” 成员之后。如果允许尾随逗号,情况会是这样:

{
+ "x": 0,
  "a": 1,
  "b": 2,
  "c": 3,
+ "y": 4,
}

It’s the exact same text transformation regardless of where we add the key. In the current model, we instead have this: 无论我们在哪里添加键,文本转换的方式都是完全一样的。而在当前的模式下,我们不得不这样做:

{
+ "x": 0,
  "a": 1,
  "b": 2,
- "c": 3
+ "c": 3,
+ "y": 4
}

Those are different transformations! Similarly if you want to remove an element, you can’t just delete the corresponding line, you have to delete the line and then check that the last line doesn’t have a trailing comma. Don’t even get me started on all the special cases involved in swapping two lines. 这些是完全不同的转换方式!同样,如果你想删除一个元素,你不能仅仅删除对应的行,你还必须删除该行,然后检查最后一行是否带有尾随逗号。更不用提交换两行时涉及的所有特殊情况了。

JSON isn’t the only language with this problem. Haskell writes record types like this: JSON 并不是唯一有这个问题的语言。Haskell 的记录类型写法如下:

-- from https://play.haskell.org/
data Drone = Drone {
  xPos :: Int
, yPos :: Int
, zPos :: Int
}

This “partial bullet point” style of putting separators at the beginning of rows makes it easier to change the last row but harder to change the first one. TLA+ has this problem too: 这种将分隔符放在行首的“部分项目符号”风格,使得修改最后一行变得容易,但修改第一行却变得困难。TLA+ 也有这个问题:

\* both valid
VARIABLES a, b, c
vars == <<a, b, c>>

\* both invalid
VARIABLES a, b, c,
vars == <<a, b, c,>>

This one’s annoying because 1) you’re constantly adding new top-level variables while working on a spec and 2) the PlusCal DSL does not have this problem: 这很烦人,因为 1) 你在编写规范时会不断添加新的顶层变量;2) PlusCal DSL 就没有这个问题:

\* Totally fine!
(*--algorithm foo {
  variables a; b; c;

The worst offenders, IMO, are logic languages like Prolog. Not only don’t you have trailing separators, you have a special terminating symbol: 在我看来,最糟糕的是像 Prolog 这样的逻辑语言。它们不仅没有尾随分隔符,还有一个特殊的终止符号:

foo(A, B, C) :-
  A = 1, % comma
  B = 2, % comma
  C = 3. % period!

I guess you can sort of think of it as funny-lookin’ braces: 我想你可以把它看作是一种长相滑稽的花括号:

foo(A, B, C) :-
  A = 1,
  B = 2,
  C = 3
.

But this is not standard syntax and people will look at you weird if you try it. And you still don’t get trailing separators. 但这并不是标准语法,如果你尝试这样做,人们会觉得你很奇怪。而且你依然无法使用尾随分隔符。

Something better

更好的方案

Some languages allow trailing separators: 有些语言允许尾随分隔符:

// go
valid := map[string]int{
  "a": 1,
  "b": 2,
  "c": 3,
}
# python
valid = {
  "a": 1,
  "b": 2,
  "c": 3,
}

But I think we can do one better than that. Python and Go commas can trail but not lead, meaning we can’t go 100% bullet points: 但我认为我们可以做得更好。Python 和 Go 的逗号可以尾随但不能前置,这意味着我们无法实现 100% 的项目符号风格:

# python again
invalid = {
  , "a": 1
  , "b": 2
  , "c": 3
}

Now I personally think that bullet points are the bee’s knees and wish more languages allowed leading separators. TLA+ actually has leading conjunction and disjunction operations: 我个人认为项目符号风格非常棒,希望更多的语言能允许前置分隔符。TLA+ 实际上就有前置的合取和析取操作:

// Not TLA+ but the same semantics
|| && a == 1
   && b == 2
|| && a == 3
   && b == 4

You can’t trail these, though, no writing (a &&). The most flexible I’ve seen is Alloy, which allows both leading and trailing commas: 不过你不能尾随这些操作,不能写成 (a &&)。我见过最灵活的是 Alloy,它允许前置和尾随逗号:

// Alloy
sig Valid {
  , a: 1
  , b: 2
}
sig AlsoValid {
  a: 1,
  b: 2,
}

Alloy does go a little power-mad here, because it also allows empty separators. Alloy 在这里确实有点“权力过大”,因为它甚至允许空分隔符。

sig StillValid {
  ,, a: 1,,
  ,,,,,,,,,
  ,, b: 2,,
}

I’ve heard some people call this “stuttering”. I can’t figure out how to commit crimes with this but you never know. 我听说有人称之为“口吃”。我还没想出怎么利用这个来搞破坏,但谁知道呢。

Devil’s advocate

反方观点

One argument against trailing separators is that they make parsing ambiguous. Consider this Prolog: 反对尾随分隔符的一个论点是它们会使解析变得模棱两可。看看这个 Prolog 代码:

foo(A, B) :- A = 1, B = 2.
bar(c).

Here it’s pretty clear that foo and bar are separate definitions. But if we replace the rule terminator with commas: 这里很清楚 foo 和 bar 是两个独立的定义。但如果我们用逗号替换规则终止符:

foo(A, B) :- A = 1, B = 2,
bar(c),

Now it could be alternatively parsed that bar(c) is part of the definition of foo— foo is only true when bar(c) is also true. As another example, this is valid Ruby: 现在它可能会被解析为 bar(c) 是 foo 定义的一部分——即 foo 仅在 bar(c) 也为真时才为真。再举一个例子,这是合法的 Ruby 代码:

# prints 5
puts 3.succ().succ()

If we could “trail method calls”, this is ambiguous: 如果我们能“尾随方法调用”,就会产生歧义:

foo.
bar().
baz().
quux()

Now it’s not clear if quux() is a top-level function or a method of foo. Both of those relate to control separators, not data separators. 现在不清楚 quux() 是一个顶层函数还是 foo 的一个方法。这两者都涉及控制分隔符,而不是数据分隔符。

Python has an edge data case with trailing data separators. The language uses parenthesis both for expression grouping like (2+3) and for tuple definition like (2,3). So how do you distinguish an expression evaluation from a single-element tuple? With a trailing comma! Python 在数据分隔符方面有一个边缘情况。该语言使用圆括号既用于表达式分组(如 (2+3)),也用于元组定义(如 (2,3))。那么如何区分表达式求值和单元素元组呢?用一个尾随逗号!

>>> x = (2+3)
>>> type(x)
<class 'int'>
>>> x = (2+3,)
>>> type(x)
<class 'tuple'>

Okay that’s all I got. 好了,这就是我想说的全部内容。