开放世界中的闭环
以我的习惯,这一篇大概应该放在知乎上。但是我觉得自己的想法很业余,不想冒着误人子弟或者没事找喷的风险。
很多编程语言中,所有特性都是为开放世界准备的。
这些语言所假设的是一个具有无限拓展可能性的开发模式。如果你定义了一个接口,那么只要这份代码一天存在于这个世界上,就有可能会有对这个接口新的实现出现。
当你拿到一个接口定义,放在面前端详的时候,你永远不会知道它的全部实现到底是个什么样的集合。
虽然我们身处现实世界,所有的实现都是人类一个一个写出来的,所以这必将是个有限集——
纵向来看,未来会有更多的实现被写出来,而其内容是无法预知的;
横向来看,跨过编译单元甚至静态链接单元的边界,也会有未知的存在,只要ABI能对的上一切皆有可能。
以前不但接口,甚至每一个数据结构都是可以被拓展的。
不过后来很多人发现了混乱,于是引入了sealed,引入了case class。
还有的人则干脆沿着相反的方向,全心全意拥抱混乱邪恶,走向duck type,引入structural typing。
另一个看待的角度是,这些语言的设计其实遵循着某种模块化的哲学。尽量最小化对代码所运行的环境的假设,从而最大化代码被复用的可能性。用KISS的方式来诠释FOSS的精神,如果强行上升一下子的话。
当然说不定只是路径依赖而已,因为以前的语言都长这个样子。谁知道呢。
在这个开放的开发模型中,也许已经有人不认同了。他们意识到复用发生的很少,转而制造one big thing。
我的见识比较少,只能举出LLVM一个例子。其经典的自定义RTTI足以说明它的态度。
实际上,对LLVM进行复用的时候,也不是非得改它的源代码。比如自行编写一个pass,是可以编译成一个独立的动态链接库,在运行时传参数使用的。
但我觉得LLVM应该是公认的不模块化的。看看常用类上定义的几百个方法吧家人们。浏览一遍,你会感觉自己看遍了世界上所有的架构。
有趣的是,作为本篇论证中封闭的一方,Rust却是遵循着开放世界假设的。
这是指在提供trait的实现时,添加额外的约束来避免任何潜在的实现冲突。以前这个规则写的挺难懂的,后来变成了
error[E0210]: type parameter `T` must be covered by another type when it appears before the first local type (`Foo`)
--> src/lib.rs:3:6
|
3 | impl<T> From<Foo> for T {}
| ^ type parameter `T` must be covered by another type when it appears before the first local type (`Foo`)
|
= note: implementing a foreign trait is only possible if at least one of the types for which it is implemented is local, and no uncovered type parameters appear before that first local type
= note: in this case, 'before' refers to the following order: `impl<..> ForeignTrait<T1, ..., Tn> for T0`, where `T0` is the first and `Tn` is the last
……反正我是觉得好像也没办法让它变得好懂了的样子。
总之,Rust眼中的世界,是一个很现实的模样:这个世界充满了对每一个trait的实现。想要添加新的实现,必须要挤进这满满当当的世界中,确保不会和任何一个假想的现存实现产生冲突。
正是由于Rust尊重开放世界所带来的根源上的复杂,才导致了这个语言特性被设计的如此受限。这是一种很清醒的设计。
我发现Rust当中出现了一些前所未见的东西:为封闭开发模式准备的语言特性。
设想LLVM的RTTI如果用sum type来写,应该会非常自然吧。
Rust当中是有一些这样的东西,你从一开始就放弃和开放世界合作,确定所有的实现都在自己的掌控之下时,才能用这些东西。
我相信这是务实的体现。
有了这些封闭式特性以后,就可以大刀阔斧地限制其他开放式特性。反正退路已经准备好了。
Rust很适合写one big thing的场合。无论是中到大规模的应用,还是什么高度自定义的库。Rust是有优越性的。