Nontrailing separators do not spark joyNontrailing separators do not spark joy
Nontrailing separators do not spark joy
Hillel Wayne
这是合法的 JSON:
{
"a": 1,
"b": 2,
"c": 3
}这是非法的 JSON:
{
"a": 1,
"b": 2,
"c": 3,
}区别就在最后一个逗号。JSON 语法规定,逗号用于分隔对象中的两个成员,但不能跟在成员后面(“尾随”)。我认为这是一个设计失误。假设我们要在这个结构体中添加两个新键,一个在 "a" 成员前面,一个在 "c" 成员后面。如果允许尾随逗号,代码看起来会是这样:
{
+ "x": 0,
"a": 1,
"b": 2,
"c": 3,
+ "y": 4,
}无论在哪里添加键,所做的文本修改都是完全一样的。但在当前的模式下,我们却得这样做:
{
+ "x": 0,
"a": 1,
"b": 2,
- "c": 3
+ "c": 3,
+ "y": 4
}这可是不同的修改操作!同样地,如果你想删除一个元素,不能仅仅删除对应的行1,你必须删掉那一行,然后再检查最后一行是不是多了一个尾随逗号。更别提交换两行时牵扯到的各种边界情况了。
JSON 并不是唯一存在这个问题的语言。Haskell 是这样写记录类型的:
-- from https://play.haskell.org/
data Drone = Drone
{ xPos :: Int
, yPos :: Int
, zPos :: Int
}这种将分隔符放在行首的“残缺项目符号”风格,使得修改最后一行变得更容易,但修改第一行却变得更麻烦了。
TLA+ 也有这个问题:
\* both valid
VARIABLES a, b, c
vars == <<a, b, c>>
\* both invalid
VARIABLES a, b, c,
vars == <<a, b, c,>>这个问题很让人头疼,因为 1) 在编写规约时,你会不断地添加新的顶级变量,而且 2) PlusCal DSL 却没有这个问题:
\* Totally fine!
(*--algorithm foo {
variables a; b; c;在我看来,最让人抓狂的是 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 的逗号可以放在尾部,但不能放在开头,这意味着我们没法百分之百地采用“项目符号”风格:
# python again
invalid = {
, "a": 1
, "b": 2
, "c": 3
}就我个人而言,我认为“项目符号”风格简直棒极了,希望更多的语言能允许前导分隔符。实际上,TLA+ 就支持前导的合取和析取操作符:
// Not TLA+ but the same semantics
|| && a == 1
&& b == 2
|| && a == 3
&& b == 4不过你不能把它们放在末尾,也就是说不能写成 (a &&)。
我见过的最灵活的语言是 Alloy,它同时允许前导和尾随逗号:
// Alloy
sig Valid {
, a: 1
, b: 2
}
sig AlsoValid {
a: 1,
b: 2,
}Alloy 在这点上确实有点“放飞自我”了,因为它甚至允许空分隔符。
sig StillValid {
,, a: 1,,
,,,,,,,,,
,, b: 2,,
}我听人把这叫做“口吃(stuttering)”。我想不出这种特性能用来干什么坏事,但谁知道呢。
唱点反调
反对使用尾随分隔符的一个理由是,它会导致解析时产生歧义。看看这段 Prolog 代码:
foo(A, B) :-
A = 1,
B = 2.
bar(c).很明显,foo 和 bar 是两个独立的定义。但如果我们把规则终止符替换成逗号:
foo(A, B) :-
A = 1,
B = 2,
bar(c),这就产生了另一种解析方式:bar(c) 成了 foo 定义的一部分——即只有当 bar(c) 也为真时,foo 才为真。
再举一个例子,这是合法的 Ruby 代码:
# prints 5
puts 3.
succ().
succ()如果我们可以“尾随方法调用”,就会产生歧义:
foo.
bar().
baz().
quux()现在就不清楚 quux() 到底是一个顶层函数,还是 foo 的一个方法了。
这两个例子都与控制分隔符有关,而不是数据分隔符。对于尾随数据分隔符,Python 有一个极端的数据场景。该语言将圆括号既用于表达式分组(如 (2+3)),又用于元组定义(如 (2,3))。那么,你该如何区分一个表达式求值和一个单元素元组呢?答案就是加一个尾随逗号!
>>> x = (2+3)
>>> type(x)
<class 'int'>
>>> x = (2+3,)
>>> type(x)
<class 'tuple'>好了,我能想到的就这些。《Logic for Programmers》全新的(也是最终的)预览版将于下周发布。
需要完整排版与评论请前往来源站点阅读。