一些有趣的语法设计的细节

by

这是一篇想写很久了的文章,是关于编程语言的语法设计的。 本文面向初学者高度友好,萌新们不要因为看到语法设计就红叉哦。

本文可能更多是站在实际使用的角度考虑问题, 可能引起部分读者身体不适,请谨慎阅读。

下面就两个细节点说说,宏观的设计我还没有资格讨论。

合法的标识符

什么样的字符串应该是合法的标识符呢?

在大家熟悉的 C 语言中, 合法的标识符的定义是这样的(使用了简单的正则):

identifier ::= nondigit
             | identifier nondigit
             | identifier digit

nondigit ::= [_a-zA-Z]
digit ::= [0-9]

另外,关键字不能是标识符(比如 int int = 233 不合法)。 这个规矩比较难使用正则描述,一般是单独写出来的。
先忽略 MSDN 一上来就左递归的恶劣行为,看看这个定义。

它禁止使用数字开头作为 identifier ,因此我们就不能写 1st 2nd 这样的变量名了。 在现代社会,大家应该都适应了这一点,所以读者们应该觉得没什么大不了的。

但是我们可以再想想。熟悉各 Lisp 方言的同学们应该知道, 在这门使用 S-Exp 的语言中, 形如 call/cc, shift/reset 这种东西也都是合法的标识符。 这是因为 S-Exp 的解析的 Tokenize 策略和 C 系语法的不同所导致的。

在初步设计 Lice 的时候, 我就决定打破这个平衡 —— 我让全部无法被解析为字面量或者注释或者括号的 token 都成为合法的标识符。
这样,我就可以使用 1st 2nd 这样的标识符了。

我的自以为是并没有持续多久,在后来加入了字符串的解析后, 我试图测试一些字符串转义的例子,比如这样的代码。

"boy \next door ass \we can"

按理说前者应该是个正确的转义(\n),后者是错的(\w),所以应该在后面报个错。 可是根据我之前的『无法被解析为字面量的疑似字面量就会被解释为标识符』的规则, 我得到了这样的报错(当然,我当时写的格式是啥样的记不得了,大概就这意思):

Unresolved reference: <"boy \next door ass \we can">

也就是说这种 100% 是写错了的情况,我还是强行将其解析了, 然后产生了不符合人类直觉的后果。
很明显这是编程语言设计中的大忌,一个相当知名的反面教材就是 JavaScript (主要是它的 implicit 太不符合直觉了,不像 OCaml/Coq/Agda/Idris 那种)。

所以我后来还是老老实实地去掉了。

但是去掉的时候我又想到了另一种解析套路,即首先将所有 『仅由数字字母下划线组成的 Token 』归为一类,然后再重复之前的解析规则, 可以保证不出现写错的字符串被识别为标识符的尴尬情况。

后来,在验证对 16 进制的支持时,我和小伙伴发现我的第二个想法也是不好的, 0xabcdefg也会被识别为标识符。

所以说实际上 C 语言当年那么设计是有道理的, 直接使用第一个字符区分了标识符和字面量。
至于各种进制的语法设计(其实就是 16 进制)也使用了一个 0 开头, 就只是因为需要一个数字在最开头占位,而 C 语言选择了 0 而已。

而 Scala 和部分 Lisp 方言的 Tokenize 规则不同,就另当别论了。 毕竟,还是有很多人想支持 a+b 这样的没有空格的二元表达式的。

代码块结束符

有的编程语言用一个单词和 end 作为代码块(Ruby, CovScript, Julia, Pascal, Lua), 有的则采用一个单词配合一对大括号。
这中间,语言设计者们应该是有过很痛苦的心路历程的。

有些不太理解语言设计、历史故事和 legacy 的小朋友, 在网上提了相关问题, 摆出了『使用end是落后或者 legacy』这种言论。

因为 Julia 的泛型参数是使用花括号的(而其他大多数语言选择了尖括号(就是大于小于, 我不知道这个说法是否准确就专门说明一下),比如 Java/Kotlin/C++ ,而这些语言, 就我所知, Kotlin 无法 Tokenize >=,C++ 以前无法 Tokenize >>, Java 把泛型参数写类型前面被喷说是丑;再看一些其他例子,Scala 选择了中括号, 于是数组下标访问和函数调用的语法就混在一起了(其实这个没什么问题,只是不符合习惯 ),所以这个问题可以说是万年老坑,大家都在换着花样想办法解决这个问题。而 Julia 只是创新性地选择了一种我们不习惯但是没有任何问题的做法而已)。

至此,花括号作为泛型参数的包裹符号的原因就有了。

站在『统一性』和『可读性』的角度来看花括号、end、泛型参数、 区块结束符这四个东西的两两组合,我们发现,泛型参数由于一般周围不加空格 (比如a.<String>b()a.b{A}()或者fun <T> f()),那么它不适合end (想想为什么)而适合花括号;而区块结束符一般都是另起一行级别的东西了, 因此花括号和end都适合。所以这么一想就能明白别人设计的意图。

至此,我们开始感性地认为end和花括号应该各司其职了。

你前面说的我也明白,但我就是觉得end丑,为什么不能泛型参数、区块结束符都用花括号呢?

而如果单纯地看语法本身的话,end可以和多种开头来配对,形成多种不同类型的代码块, 比如 Julia 的begin endquote endlet end等, 花括号实现类似的效果一般都是再另外弄个开头,这样的话就增加了一个词法元素, 令人有些许不爽,不过这其实没什么,你们语言用户觉得没什么, 他们语言设计者也觉得没什么,我们 IDE 开发者也觉得没什么。但是考虑到歧义就不一样了, 我随便举个例子就可以了。

代码:

function f{A}(a :: A)
    return yes yes yes oh my god 
end

假设Julia使用了花括号作为区块结束符:

function f{A}(a :: A) {
    return yes yes yes oh my god 
}

那么,上面那段代码,其实会被解析为:

function f {
  A
}
(a :: A)
{ return yes yes yes oh my god }

然后疯狂报错。

所以尽可能让各个词法元素的语义也区分开吧。

顺带一提, Julia 的 function a end 是一个返回一个函数的、有定义函数的副作用的表达式 。

现在这Julia的语法的海量歧义搞得我对它的印象完全就是MIT的PL民科设计的语言,而且他们Gitter里有人说什么

Julia is more Lispy comparing to Haskell

,即使抛开 Haskell 这个我提到的东西,也可以体现出这些人的很多知识漏洞, 比如对 Lisp 一无所知(不过似乎开源哥不赞成这个说法。。。不是很懂?), 我实在不知道 Julia 除了 Parser 所使用的语言是一门 Scheme 方言之外它到底还和这四个怪名乱神的字母有什么关系。


Tweet this
Top


创建一个 issue 以申请评论
Create an issue to apply for commentary


协议/License

本作品 一些有趣的语法设计的细节 采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,基于 http://ice1000.org/2018/02/18/SyntaxDesign/ 上的作品创作。
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
知识共享许可协议