The Perfect Language

如果有一种语言是完美的,那它应该是什么样子的?谈谈我的看法。

防止用户用石头砸自己的脚

C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do it blows your whole leg off.

这句话出自Bjarne Stroustrup。即便C++让搬起石头砸自己的脚看起来更难一点,但并没有从根本上阻止用户这么干。根本问题在于用户是靠不住的,他可能不知道哪些行为是危险的。C++里面有不少这种一不小心就砸到自己的特性。比如指针和引用,每个程序员应该都有被C++的指针和引用伤害过,用的姿势不对就会引发灾难。所以写C++程序一定要小心翼翼,虽然我已经写了很多年了,但还是保持着“敬畏之心”,每次不确定某个写法的时候都要查资料反复确认。还有一些本来就可有可无的特性,引入之后反而让事情变得更复杂,但是又没有带来任何收益。比如i++++i,写个i = i + 1能又多难呢?这都是导致程序员脱发的元凶啊,生活本来可以更简单的。

那应该如何防止用户干这种事情呢?在指针和引用的问题上,Rust的处理方案是非常优秀的,通过ownership和borrow checker完美的解决了这类问题。只要用的姿势不对,编译都过不了。虽然代码写地略痛苦,但是和上线之后才发现问题相比,这点痛苦完全可以忽略不计了。

有问题就应该在编译阶段就解决,运行时再发现已经太迟了。

Maybe, Option, Optional

如果有一个整数型的变量,如何判断它是否有值呢?下面是一种常见的做法。

int write_x();

int read_x(int x) {
    if (x == 0) {
        do_something_without_x();
    } else {
        do_something_with_x();
    }

    return 0;
}

read_x能否正常运行,取决于write_x的实现。如果write_x也一样认为0就是没有的意思,那就万事大吉,否则就会出翔了。假如由两个团队分别维护read_xwrite_x两个函数,出现问题的机率更大。即使某个时刻双方达成了约定,无法通过系统进行约束,约定就是废纸一张。当然可以尝试通过其他途径进行封装,比如protobuf的has_x(),或者是将x的访问统一封装成函数库。只要有机会可以直接操作x,都无法彻底的避免。如果在语言层面支持,结果就非常不同了。

这里需要的是一种类型,可以完美的描述某个东西不存在的场景。这个类型,就是Haskell的Maybe,Scala和Rust中的Option,Swift的Optional。如果用Scala的话,上面的代码应该是这样的。

def readX(optionX: Option[Int]): Unit = {
    optionX match {
      case Some(x) =>
        doSomethingWithX()
      case None =>
        doSomethingWithoutX()
    }
  }

通过Pattern Match,如果忘记了匹配case None,编译器会提醒你。如果不使用pattern match,而是通过optionX.get获取,当x不存在的时候会抛异常,虽然问题发现的晚,但是可以保证不会执行一段奇怪的逻辑。

Option实际上是一种代数类型(Algebraic data type),借助ADT可以实现各种复杂的类型,详细解释可以参考Haskell的wiki

静态类型

在网上找到一张描述Duck Typing图片,可以说非常生动了。

“If it walks like a duck and it quacks like a duck, then it must be a duck.”

多年的经验告诉我,类型检查绝对是程序员的好朋友。曾经以为Python就是银弹,可以拯救世界了。但后来发现越来越不喜欢用Python了。作为典型的动态类型语言,代码量多了非常难以维护,自己写的代码都这样,更何况团队协作了。

Haskell应该是静态类型的代表了,写Haskell程序通过编译是有一定难度的,但是只要能成功编译,基本可以确定问题不大了。还有个好处是,看一眼函数的参数返回值类型,就基本可以确定它是做什么的。

对开发者友好

一个好的语言必须要关注开发者的体验。如果官方能够考虑到不同开发者的需求,支持在多种编辑器和IDE下开发,那就最好不过了。现阶段有LSP(Language Server Protocol)这种终极解决方案,可能都不需要这么麻烦,只需实现language server就好了。

用Emacs写Python体验是非常差的。不知道是什么原因,虽然emacs里面和python相关的包有很多,用下来感觉基本每个都有硬伤,达不到可用的状态。python-mode的bug多的数不过来,连缩进都处理不好。Jedi有时候莫名其妙的卡。所以很长一段时间里我都不愿意写python代码了。或许跟语言本身的设计有关系?不知现在python的lsp实现用起来怎么样,改天调研一下。

相反,用Emacs写Scala代码是非常爽的。不管是早期的ensime-mode,还是现在官方的lsp支持Metals,都给人接近完美的体验。虽然有一些小的瑕疵,但是不会有太大的影响。爽在什么地方呢?写其他语言的时候,代码跳转有可能跳不成功,自动补全有些情况下不行,反正也不知道是啥原因,但是因为有这种碰壁的情况,心理上就会比较抗拒去使用。但是写scala就完全不同了,根本不存在例外,每次都能成功。可能跟本地有所有的jar包有关?

还有一个必不可少的工具就是formatter。团队作战的情况下如何做到统一风格?全靠formatter了。C++有clang-format,python有yapf。对于formatter来说,最重要的一点就是要支持每行最大的长度,比如80个字符。如果没有这个特性,那么称不上是完整的formatter。

编译和运行依赖

从程序部署看,最痛苦的是python这种解释型语言。代码在本机写好了,部署后却发现生产环境缺了无数的包。大多数人可能觉得缺什么就装什么呗,但是在一个没有把python作为主力开发语言的公司,安装一个包别提有多难了。python本身的版本也无法选择,只能用系统自带的老版本。网络又是隔离的,根本访问不了官方源。

既然运行环境有依赖,那就选编译型语言吧。问题依然存在。生产环境的各种问题,难道在开发环境就不存在了吗?想要在公司的开发环境上使用一种新语言(比如rust),也是困难重重。只要团队没有把它作为主力开发语言,环境永远是一个痛点。

在这个方面上,基于jvm的语言完胜。首先不需要开发环境,代码在任意的机器上都可以写,因为有jvm的存在,完全不需要考虑运行的机器和系统。生产环境部署只需要安装jdk即可,虽然比编译后的二进制部署稍微复杂一点,但是完全可以接受的,下载个jdk解压即可。

完美的语言是不存在的

每种语言都有自己的长处,也都有自己的坑,完美的语言目前肯定是不存在的,都有各自的权衡罢了。我觉得好的地方,其他人可不一定这么想。但是最近几年可以明显感觉到,语言的发展越来越快了,各个大厂都开始推广自己的语言。或许在遥远的将来可以自己创造一个?