这个问题值得好好讨论一番。下面都是一家之言,如有纰漏,欢迎交流指正!
最初的起源可见 hooopo 兄十个月前发的帖子:https://bugs.ruby-lang.org/issues/9999。这个问题甚至可以从 Feature #9999 追溯到 Feature #5583。
继 Soft Typing 以后,Jeremy Siek 等人于 2006 年出来的 Gradual Typing(科普文章可见Jeremy Siel - 什么是 GRADUAL TYPING)早就在动态语言阵营中挂起一阵混搭旋风。也有很多人蠢蠢欲动往 Ruby 以及 Rails 里面加类型系统,比如:
- The Ruby Type Checker, Brianna M. Ren, et al.
- Static Typing for Ruby on Rails, Jong-hoon (David) An, et al.
- Dynamic Inference of Static Types for Ruby, Jong-hoon (David) An, et al.
是否引入类型系统,怎么引入,以及要用类型系统,都得看动机。
为了规避不必要的运行时类型检查,以提高程序的运行效率,那么引入静态类型检查,在语法分析阶段确定好运算符/方法的分派是非常有必要的,但 Ruby 骨子里的动态性让这个有些难做,或者说,只能让程序做部分的静态类型检查(基本上就是 Gradual Typing 的思想)。正如视频里面提到的那样,Go 语言的 Structural Subtyping 跟 Duck Typing 有异曲同工之妙,都是面向协议编程(或者说面向接口编程)。
反之,如果引入类型系统不当,反而会带来很多麻烦。Ruby 里面没有多态,有时候实现多态要靠参数列表来实现,请考虑下面的方法定义:
def foo(*bar)
# blah blah
end
怎么给 bar
加 annotation?Array
?Array<T>
?由于数组内容通常还可能是不同类的实例,合法的类型标注可能是 Array<Object>
,这不等于啥都没说嘛!要不要用 Dependent Type、**hash
和 &blk
又怎么处理、为了让类型系统能够元编程,是否也要让类型成为 First class citizen?这些都是问题。
部分人讨厌类型系统,是因为他们觉得写类型注释很恼人,很啰嗦。类型注释绝不是啰嗦,它其实也是一种 Specification,表明了程序员的意图。一方面来说,类型注释描述的是做什么(What to do),而具体的代码才是怎么去做(How to do)。我们有 Curry-Howard 同构告诉我们程序即证明,那么类型注释就是这个 Goal,我们编写代码让类型系统接受,就是去满足我们的 Specification,两者应该是相辅相成的。程序员觉得类型系统麻烦,只是因为在面向一些复杂类型时无从下手而已(比如一个很经典的问题就是:简单类型的 Lambda 演算系统中,没法给 Y 组合子定类型)。
类型系统还有一个更重要的方面:类型安全。虽然 Ruby 本身是动态类型的语言,但它是强类型语言,所以目前 Ruby 在类型安全这方面做得不错了。前段时间有个叫 Rubype 的 Gem,作者声称是受Gradual Type Checking for Ruby这篇文章启发,而后面那篇文章也明确 Ref 了之前提到的 The Ruby Type Checker 这篇 Paper。
这个 Gem 是在用户自定义级方法的层次上完成类型检查,再讨论 Rubype 之前,请考虑下面的定义:
class Vector
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
end
# 这里将向量的数乘(Scale)和点乘(Punktprodukt)都定义成了一个多态的 *
def *(v)
case v
when Numeric
Vector.new(x * v, y * v)
when Vector
x * v.x + y * v.y
else
raise TypeError
end
end
def to_s
"Vector: <#@x, #@y>"
end
end
由于 Ruby 是不会做参数类型检查的,所以我们要自己去检查参数类型,并根据类型来分派需要执行的操作。显然将一个向量与字符串相乘是没有意义的,因此对于其它类型来说,我们要抛出 TypeError
。实际上很多 Ruby 程序都有这样的需求,就跟函数的定义域一样,一个方法可能只是针对某个或某几个特殊的域(Domains)来定义的,对于不在这个定义域的参数,我们必须抛出一个错误,但每次都这么做,很容易违背 DRY 的原则。想一想也是,本来函数定义域这个东西,是属于“声明范畴”的,却要放在命令范畴让程序员自己写代码来做检查,确实有点离谱,所以就有了 Rubype。
可惜的是,Rubype 它只能算作是一种语法糖,只是像 attr_*
系列方法那样,通过声明式的方式,将函数的类型注释维护起来。与手动写代码不同,Rubype 不但检查参数类型,还检查返回值类型,这也使得它的 Runtime Overhead 非常严重。Rubype 没有从根本上解决问题,当然要想从根本上解决这个问题必须在解释器上开刀。不过 Rubype 的“Meaningful error”和“Executable documentation”还是比较有亮点的。
扯了这么半天,观点还是比较零碎,总结一下:
- 类型系统很重要,得加,不能乱加,Ruby 里面黑魔法太多了,外表貌似“优雅”,内部及其混乱(比如 Proc、lambda、Block);
- 类型系统跟数学结合紧密,必然会给程序员带来门槛,想想子类型系统中的 Liskov 替换原则就折磨了多少人,所以不要为了高大上就追求牛逼的数学模型合数学系统,Ruby 还是要以务实为主;
- 现有类型系统很多,选择最适合 Ruby 的类型系统很重要,至少在表达 Ruby 常用的业务逻辑时要很方便,Ruby 的宗旨是让程序员快乐,可现有类型系统无论是编译器还是程序员都开心不起来;
- 类型系统是个大头,加进来无论是解释器还是 Ruby 脚本,复杂度必然增长,如何合理控制这种复杂度,如何在动态合静态间进行取舍,这个也值得深究;
类型系统往深了扯还涉及到程序设计语言的形式语义、程序正确性等问题,最近看屈延文院士的《形式语义学》一书中有这么一个说法:
那么是否说采用 Dijkstra 方法就能保证程序正确呢?假如是一个理想的程序设计者,从不会出现任何错误,那么该方法设计出的程序是可靠的。我想,这就如同一个数学定理的证明一样,会有人证错。大概 Dijkstra 本人,如领导许多程序员按照他的方法设计程序,当程序员设计完程序后,向他说程序已经没有错误,他大概也只能相信他们是对的,而不能保证他们是对的。
突然感觉就编程这方面,我们还任重而道远啊…………
最后,Tom 那句话非常在理,这玩意儿不是脑袋一热一个 Pull Request 就可以搞定的,可能还真得一个需要 PhD Thesis(搞不好还得好多个 )。