几天前,有个人来找我,问我:‘我这语言语法设计得怎么样?’
我连语法都没看,就问:‘你这语言的设计目的是什么?’
‘我希望设计一个语言,融合FP跟OOP,这样我可以既有FP的表达力,也有OOP的易用性’
我忍着吐槽的欲望,问:‘那你有继承吧?’
‘有’
‘那Subtyping呢?’
‘当然也有’
‘那你打算怎么做类型推导?’
‘Algorithm W’
‘你该知道,Algorithm W是靠unification做推导的,你如果有subtyping,跟Algorithm W不相容,你想好这么解了吗?’
‘啊。。没有’
其实如果答出来,还有很多相近的设计问题:
- OOP提倡extensibility,但是FP的ADT是封闭的,无法扩展新类型 - 而Class/Object无法在类型安全且不更改以前代码的前提下扩展新的method,这个语言该怎么提高扩展性?
- Invariant/Covariant/Contravariant/Recursive Type/Existential Universal Quantification/Module的Subtyping怎么做?
- Ad Hoc Polymorphism呢?如果靠Typeclass,这套东西怎么跟OOP的Class System整合?如果靠implicit Coherent问题怎么解决?
这些问题不是不能解,毕竟融合FP跟OOP的语言一大推,OCaml跟Scala不说,往早说,Luca的一推paper全是这个套路,但是问题是,当你要设计语言的时候,这些东西要先考虑清楚,而不是先去考虑语法。比如说Subtyping可以看F<:跟MLSub,Extensibility可以看MLPolyR/Data a la carte/Object Algebra。
另一个我想吐槽的事情,就是‘超市买菜’模型。很多人在设计语言的时候,都会有如下的思路:‘X是好的,Y是好的,Z是好的,我也刚好都需要,所以我这个语言要有X, Y, Z。’,比如上面那个人,就认为‘FP是好的,OOP是好的,我都需要,于是我去设计语法去了’
这样做的问题是完全没考虑到各种feature之间的互相影响-而这才是设计语言最麻烦的地方。
比如说,某个语言是Dynamically Typed的,但是有个叫Type Stability的概念:如果我能静态用Dataflow Analysis分析出类型,我就会用Type Directed Compilation做传统静态类型的优化。同时,我提供Type Annotation,所以如果分析不出来还可以手动加类型。
那好,这语言有Reference(指针),这再加上Higher Order Function,跟Dataflow Analysis很合不来,随便写点高阶的东西推不出,性能突然暴死怎么办?而Type Annotation则因为这个语言有Subtyping,同时Type Annotation选择会runtime check/convert的原因,导致没有Arrow Type,所以无法给高阶函数加Annotation,而这又恰巧是最需要的时候。。(Gradual Typing性能坑,而且他们还有Union Type,这下不止性能有问题,连做都不好做,见The root cause of blame: contracts for intersection and union types)
IMO,如果设计语言,应该正好反过来:很多人都是先设计语法,然后再写实现,最后interaction管都不管听天由命的。如果是这个顺序:
- 先去考虑我这个语言的目的是什么(提示:如果你不是GRAIL,Scratch,APL,那答案不会是‘语法’)(提示:答案九成是‘我在设计这个领域的DSL’。)
- 然后去找可以实现目的的最小feature set-不可以被desugar的feature set越大,设计如上所说,越多坑,静态分析/类型推导/Partial Evaluation等操作也越难实现(要处理的case更多是一码事,不好处理的特性也是一码事(比如高阶语言不适合静态分析,指针不适合Partial Eval,Lambda Cube越往上爬越不好推类型,Subtyping更不好推等等))
- 想想看自己要实现的Pass,脑袋里面建个大概的蓝图,想想要采用啥算法,看看有没有坑(要用Dataflow analysis做分析?跟高阶函数说再见吧。HM?Subtyping不相容。想在Effectful Language里面做Dead Code Elimination?你还是高阶的?好好想想Effect Analysis怎么设计吧)
- 如果上一步发现冲突,移除一定的Feature/Pass,看看能不能被啥弥补,或者直接不要,然后接着找冲突
- 好了,开始实现各种Pass吧
- 到了最后,才到实现Surface Language/Concrete Syntax/Parser/Pretty Printing/IDE Support这些外观上的东西的地步。
那才可以在第零时间发现这些特性/Pass interact的坑,也可以有个明确的目的,而不是看到啥好的特性,就往语言里面乱塞,最后做出一个跟C++一样复杂坑多又不好扩展的语言。