闲谈程序语言设计的一致性

小编按:本文系 Rust编程 公众账号的原创文章,作者为 失落的神喵。本着共同建设Rust社区的愿景,我们两个公众账号准备一起来做。

细心的朋友们可能注意到了一个很小的问题,Rust中struct的初始化赋值,是这样的语法:

let p = Point { x:3, y:5 };

其中,每个field的赋值,用的是冒号:而不是赋值号=。

实际上,在Rust的设计过程中,这个话题被多次的提起,许多人都建议希望在这里使用赋值号。原因很好理解:为了一致性。在Rust中,既然普通变量的赋值是用=,为何在struct初始化的时候又换成了冒号呢。一般情况下,冒号后面跟着类型说明,可是在struct初始化的时候,冒号后面跟着的是初始化赋值。这一点很不可思议。

谈到程序设计语言的一致性,是个很有趣的话题。一般来说,不一致的设计非常惹人反感。一方面,它提高了学习成本,使得学习者难以触类旁通举一反三;另一方面,它降低了程序语言的美观度,容易给人一些预料之外的感受。

一致性设计是每一个体面的设计者都应当注意的问题。在神雕侠侣中,郭襄长大后与杨过的初次相逢是在风陵渡,到了倚天屠龙记中,灭绝师太告诉周芷若,她的师父、郭祖师的徒儿叫做风陵师太。权利的游戏中,詹姆把布兰从楼上推下,后来他儿子托曼也是坠楼而亡。前后呼应,因果轮回。更不用说红楼梦里让人津津乐道的草蛇灰线伏笔千里。细细品味,回味无穷,所以它们才能成为经典。

Rust的设计者一开始就将“一致性”作为一条非常重要的设计原则来考虑的。在许多地方,我们都能感受到这一点。比如,数学运算、逻辑运算、流程控制甚至语句块,全部都是表达式,它们都有类型,任一表达式都可以嵌入到另外一种表达式中。再比如,下划线的含义就是占位符,这个占位符可以是变量,可以是类型,也可以是类型参数,或者是match语句中的默认分支。

fn function(_: Foo){} 
let v : _ = vec![1i32, 2, 3]; 
let v : Vec<_> = vec![1i32, 2, 3]; 
match x { _ => default_action(); }

再比如“模式解构”,let语句能引入局部变量,它支持模式解构;函数的参数可以引入局部变量,它也支持模式解构;match语句if let while let语句,都能引入局部变量,它们都支持模式解构。不同的场景,同样的规则。清晰直观,有一种简洁的美感。

那我们回到本文开头的那个问题,来看看如果struct的初始化使用=,即foo { a = b } 是合法的语法,会产生什么问题。考虑以下语句:

if foo { a = b }

这条语句在语法解析阶段,可能会出现歧义,它可能被解析为if ( foo {a = b} ),也可能被解析为if ( foo ) { a = b}。再考虑以下语句:

for b in foo { a = b }

同样产生了歧义。foo { a = b } 是一个结构体初始化赋值,还是for语句的一部分?

这样的问题实际上有多种的解决方案,每种方案都有利也有弊。对于Rust设计者最终做的这个选择,究竟是好还是坏,笔者也不敢妄下结论,起码它不是一个非常坏的决策。

在大多数情况下,Rust的各个语法特性之间都保持了比较好的一致性。可是也有少数例外。除了刚才说的这个小问题之外,在泛型的设计上,也有一处非常明显的“不一致性”。在声明泛型的struct/trait/fn的时候,直接将类型参数写到尖括号内就可以。

struct Foo<T>{} 
trait Foo<T> {} 
fn func<T> {}

可是在调用泛型函数的时候,必须使用双冒号将泛型参数分开:

foo::<i32>();

因为如果不加上这个多余的双冒号分隔符的话,在解析以下这种语法的时候,会发生歧义:

(foo<A, b>())

这个表达式在语法解析阶段,可以有两种含义:

  1. 它可以被解析成一个泛型函数的调用,被小括号括起来了;

  2. 它可以看成一个tuple,其中有两个元素,一个是【 foo()】 。

因为逗号既可以用在 tuple 中分隔各个成员,又可以用在泛型参数列表中分隔各个参数。可能有同学又问了,那可以通过语义分析来解决这样的解析困难的问题啊,如果我们知道每个名字是类型还是变量不就可以区分开了么。可是Rust的语法分析器在设计的时候还有另外一条规则,即语法尽量做到上下文无关,越简单越好。如果语法规则变得更复杂,不仅意味着编译器的维护成本变大,更麻烦的是语法高亮、自动提示、静态分析等周边工具的开发更复杂。

当然,我们还可以通过其它方式来解决这样的冲突。比如,用单独的一种类型的括号来用于泛型,不与比较运算符冲突,不就可以了么。可是问题是ASCII编码下总共只有那么几种括号,其它的括号都有了更合适的使用场景。如果我们把选择范围扩展到ASCII编码外的UNICODE范围内选择,当然可以做到让所有的符号只有唯一的一个功能,不再有任何冲突。从这个思路发展下去,或许我们可以设计一种 emoji oriented programming。excited!

实际上,在绝大多数的编程语言中,都碰到过各种设计目标之间的冲突的问题,或多或少的存在着这样或者那样的“不一致性”。

拿C++来说,举个简单的例子,C++11标准以前,class 里面的 static 变量,只有整数类型可以直接在头文件中初始化,其它类型比如浮点数必须写到 cpp 文件中初始化。这样的限制毫无道理。

拿Java来说,基本类型是特殊处理的类型,它们不能被用在泛型参数中。

拿我们一个生活中的楼层作为例子来说,也存在不一致性。一般地面上的那一层,我们称作“第一层”,往上一层呢,就叫“第二层”,依此类推。对于地下的楼层,可以称作“负一层”,继续往下,就叫“负二层”,依此类推。看起来很一致,对不对。但是请大家再想想,从x层走到y层需要爬几层楼?如果直接使用y-x来计算对么?凭什么从-1楼往上爬一层就到1楼?-1+1=1符合直觉吗?

保持一致性一直是Rust设计者念兹在兹的一条设计准则。但在所有的地方都做到一致性非常不容易,关于这个问题,Perl 语言的设计者 Larry Wall有非常精辟的总结,一语中的:

The problem with being consistent is that there are lots of ways to be consistent, and they’re all inconsistent with each other. —— Larry Wall

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值