编程语言的动静之争:Clojure太灵活,我们该如何驾驭它?
注:本文整理自 Morgan Stanley VP 何婧誉在 QCon 2017 北京站上的演讲,原题为:《属兔的处子——喜欢 Clojure,但怕动态语言太灵活怎么办》。
古话说的好,静若处子,动若脱兔。这个我觉得非常适合形容动静态语言的区别,静态语言因为类型系统的关系,一直给人的是很稳定、很可靠,但是可靠到一定程度就变成了死板,会变成一个牢狱或者困住业务上所需的灵活性,因此常常需要很多层抽象,很多层胶水代码,代码就开始变得非常的晦涩,非常的难懂,而动态语言则完全是相反的。
常听到有人说,Clojure 确实优美,但动态语言实在驾驭不了啊!没有类型的帮助,在涉及到复杂的数据结构之后很容易失去对现有程序的理解,易读性也会急速下降,而这也确实是 Clojure 作为动态语言所造成的问题。但是部分解决这个问题的办法总是有的。core.typed 和 core.spec 两个核心库就可以帮助我们缓解动态语言太过野性框不住的问题,而本次演讲的任务就是向大家介绍这两个库,以及这两个库解决这一问题的不同角度。
静态语言因为类型系统的关系,一直给人的是很稳定、很可靠,但是可靠到一定程度就变成了死板,会变成一个牢狱或者困住业务上所需的灵活性,因此常常需要很多层抽象,很多层胶水代码,代码就开始变得非常的晦涩,非常的难懂。动态语言则完全是相反的,所有东西都是从类型上来讲,以函数为例,灵活性已经足够了,但是通常我们写着写着就忘记数据长什么样子了,你可能今天写了一个函数说,输入一个函数的数据,然后过了一个星期之后,我已经完全忘记这个数据是什么东西了,因为生产环境里面,类型系统在没有编译器的帮助下,基本上都是一次性的,这个问题对于用户来说有相当大的困扰。
一直以来,这两派之间没有争出特别的高低,静态语言笑动态语言做不出大系统,动态语言笑静态语言写的太慢、废话太多,今天这个主题当然不可能解决这个纷争,但是希望通过 Clojure 这个语言可以给大家一些不太为人知的思路。马上就有人来问了,我写 Clojure 就是为了逃避这样的内容来写系统,这样灵活多好用啊,我想写什么就写什么,快速原型靠的就是这个,我非常同意这一点。
在 Clojure 里面有一个 json,因为动态语言的关系相当的简单,完全没有废话。这个函数我觉得哪怕是不写 Clojure 的,这个也是应该很能读的懂的。首先有一个 Java 的 Reader,是 FileReader,这个 Reader 被传递到了这个 json 的函数里面,读出来文件内容,读到 Map 里面,但是读完之后,你知道数据长什么样吗?不知道,下次换一个 json 文件,同样的函数可以同样读,但是你不知道读出来是什么东西。讲到这里就已经有一点难度在里面了。
现在看一下,我现在读完了要处理,我处理之后,我写任意一个函数,如果说你不看这个函数写的什么东西,你知道它处理完成之后长什么样吗?不知道,你知道他希望这个 json 数据是什么样的形状吗?不知道。我现在看了代码之后,可以给你讲,它里面会有会有 Age、Name、Job、Address。
看一下 Age,它需要能够使用 Int,那应该是个整数,但是要看代码才知道,再下面还是简单,那你们觉得 Name 的值是什么东西?完全没有使用到,它是一个 String,它是不是姓和名放在一起了?还是放在一个 Vector 里面,可能姓和名是分开的,就是说不知道,要看代码才知道。
你看到代码之后觉得,原来是这样,它应该是一个 Vector,或者是 List,姓和名是分开放,因为它这系,它用空格来 Join 一下,这个是一个很浅显的例子,就已经说明了 Clojure 的动态灵活性非常强,但是也造成对数据的解释性标记不是很清楚。
前文是一个很浅显的例子,现在来看一个更具体的。为了这个主题我想了好几天,觉得还是写一个很小的项目来展示一下要讲的东西。那写什么东西呢?我又想了好几天,在此先谢谢链家。因为是这样的,既然要来北京,就要关注一下房价。我到网上去看二手房信息,但一页页翻过去很累,我不可能手写一个总结,于是就写点程序抓取。当然这里不是真的写了一个爬虫,只是抓几个页面做做样子,没有让链家服务受到伤害,请鸟哥放心。
命名空间做的基本上就是通过一个库把 html 读进来之后,进行一些简单的操作,把整理好的数据写到一个 EDN 文件里边。比如说第一条你可以看到这个小区 1150 万,三卧室两个客厅,一个厨房两个卫生间,包括面积之类的东西。再看这个数据转换的函数,它收到一个参数是 Page,但这个 Page 长什么样完全不知道。我是通过库读进来的,读进来之后,并不知道它长什么样子,现在看这个代码也很难知道,它到底会返回一个什么样的类型,什么样的数据,如果将来需要扩展的话,或者将来我要给另外一个人用,或者帮助另外的一个人去做一些扩展,做一些维护很难搞定。
这就是前文说的 Clojure 作为一个动态语言的弊端——太灵活。这个弊端导致经常会忘记函数的参数是什么样子,而且这个是小项目,项目一大,那就更麻烦。可能有人会说的,文档不就是做这个事情的吗?文档跟测试,没有紧密的联合在一起,文档本身的代码是剥离的,而相对代码本身是没有限制的。比如说很多代码上面会写,但是其实代码里面并没有,它可能起到的效果某种程度上也是挺有限的。
Core.typed 是一个类型系统。它和其他语言的类型系统还是有点不一样的地方,不同点在于它不是语言的一部分,而是一个即查即用的库。Lisp 的灵活性导致它能够作为一个库直接插进去,而不是要作为一个语言核心。因为它有宏,通过宏可以把一个很大的类型系统直接插进去,而且这个类型系统比一般的系统要灵活很多,主要体现在这几个方面:
第一,它可以给已经写好的,没有标注过的,或者说是用的库里面没有标注过的函数直接加上类型;
第二,不需要把所有函数全部加上类型,你不想要的话,就不需要;
第三,你即使加上了也不一定要进行类型检查,所以它是一个选择性非常强的东西。它是为了能够和 Clojure 这样的语言进行协作。
那我们现在看一下它支持什么东西:
OptionType,现在很流行,这个流行的语言现在都有这个结构。
Ordered Intersection Type 这个我不多讲了,这个就是说一个函数,比如有两种参数形式,这两种参数类型可能又不一样,你再进行类型检查的时候,它会把这个参数从上到下有序的来进行一个匹配。unionType,写过 Haskell 人都知道,这个很简单,比如说整数,或者说是字符串,把它 union 一下,那就表示这个类型里面的东西可以是字符串,也可以是函数。
Identity 是很简单的函数,它会给你一模一样的东西,那它的类型是什么呢?它这个函数的类型是什么东西呢?让 Core.type 来帮我看一下。这个基本上可以看到前面有个 all,在这里对所有的 X 能取得的类型它返回的是一个 X,就是 Polymorphism 最简单的一个体现了。Occurrence Typing 这个东西见到的比较少,它是什么呢?它是通过检查代码里面写的控制流,比如像 if,或者像 switch,它能够进行类型推导。
举个例子,首先把这个 Form 绑到 A 这个名字上面,值就是 1,但是我把它标注成了 any,就是说这个 A,就算只是 1,然后再返回 A,这里大家觉得会返回什么东西?如果是检查一个类型,它最后返回的是 A,它是什么类型?Any,因为我已经标过了,我说 A 是 Any,所以它就相信 A 是 Any,但是如果我这么写,这个会返回什么东西?你可以看到它现在还是返回的是 A,这个 A 或者这个也是 A,那其他情况返回的是 Nil,那他现在觉得这个东西是什么呢?还是不是 Any,因为你现在有了控制流在这边,代码里已经写过了,所以它知道你只可能是 number,或者是 string,要不然就是 nil,所以最后 A 是 union string/number/nil。这个东西功能上是非常强大的,这个也是我强推的一个东西,这个你真正用起来就知道方便。
最后一个就是宏也会被展开之后再推导类型,宏跟大家刚刚知道的 switch 有点像,就是已经很直接了当的,告诉大家这个宏是可以展开之后判断类型。我做的 Demo 的 types Demo,就是把刚刚链家那个小项目加了类型系统,我现在是把它所制造的结果定义类型,但它其实是什么呢?是一个 Map。
Core.spec 总结
通过一个库给动态语言加上类型系统——即插即用
可以给已经写好的函数或者是用的无类型库标注类型
可以选择性地加上类型
加上了类型也并非一定要 type check
支持 Option Type,Ordered Intersection Types, Union Types
支持 Heterogenous Maps 和 Sequentials
支持 Polymorphism (All, Context Bounds),Higher-Kinds
支持 Occurrence Typing!(通过检查 control flow 进行类型推导)
宏也会被展开后再推导类型
我本人很喜欢写这个,我觉得给函数加上类型非常过瘾,但是有问题,那有别的办法吗?有的,Core.spec,现在这个东西是 Clojure 核心,在很尽力地推广。在方法上或者在函数上,加上先限条件,功能要强大一点,强大在什么地方呢?
比方说生产环境,Runtime 不会受到影响,它的性能不会受到影响。因为如果你一天到晚在检验,它的性能上是会受到影响的,所以缺省验证是关闭掉的,如果你觉得某些东西可能重要性比较大,你要加上去也是可以的。spec 非常灵活,它可以把那种正则方式的 rule 给写起来,就是比如某个 list,我觉得里面开头至少有一个字符串,然后后面跟着的至少是 0 个的整数等等,你就可以用正则里面的加号,星号直接定义这个 rule。并且所有只有一个参数的 predicate 的函数统统可以跟它进行无缝对接,不需要另外语法把它转换成 spec。这里面有很多种的验证方式,那么多的验证的方式可能现在没有时间讲,就不讲了,总体来说就是可以把数据套在一个很灵活的模子里。
Core.spec 总结
Runtime 性能基本不会受到影响(缺省 spec 验证关闭)
Map 的类型应该就是 key 及其对应的值的类型!(keys)
Sequence 可以多方面限制(cat, alt, regex style matching, coll-of)
只有一个参数的返回 boolean 值的函数通通都自动成为 predicate
各种验证方式,满足你的需求 (conform, explain, valid?)
multi-spec 支持更复杂的数据结构
core.typed 和 core.spec 你推荐哪个?
我的脑子喜欢 core.spec,因为有前景。我的内心喜欢 core.typed,因为给东西加类型写起来真得很过瘾。