编程语言的动静之争:Clojure太灵活,我们该如何驾驭它?

摘要: 编程语言的圣战,除了语言种类之分,也有动静门派之别。我们写着静态语言往往想着动态语言的灵活,写着动态语言又容易想着静态语言的稳定和可靠。常听到有人说,Clojure 确实优美,但动态语言实在驾驭不了,怎么办?

11-16 22:35 首页 InfoQ
作者|何婧誉
编辑|小智
编程语言的圣战,除了语言种类之分,也有动静门派之别。我们写着静态语言往往想着动态语言的灵活,写着动态语言又容易想着静态语言的稳定和可靠。常听到有人说,Clojure 确实优美,但动态语言实在驾驭不了,怎么办?

注:本文整理自 Morgan Stanley VP 何婧誉在 QCon 2017 北京站上的演讲,原题为:《属兔的处子——喜欢 Clojure,但怕动态语言太灵活怎么办》。

写在前面

古话说的好,静若处子,动若脱兔。这个我觉得非常适合形容动静态语言的区别,静态语言因为类型系统的关系,一直给人的是很稳定、很可靠,但是可靠到一定程度就变成了死板,会变成一个牢狱或者困住业务上所需的灵活性,因此常常需要很多层抽象,很多层胶水代码,代码就开始变得非常的晦涩,非常的难懂,而动态语言则完全是相反的。

常听到有人说,Clojure 确实优美,但动态语言实在驾驭不了啊!没有类型的帮助,在涉及到复杂的数据结构之后很容易失去对现有程序的理解,易读性也会急速下降,而这也确实是 Clojure 作为动态语言所造成的问题。但是部分解决这个问题的办法总是有的。core.typed 和 core.spec 两个核心库就可以帮助我们缓解动态语言太过野性框不住的问题,而本次演讲的任务就是向大家介绍这两个库,以及这两个库解决这一问题的不同角度。

静态语言 VS 动态语言

静态语言因为类型系统的关系,一直给人的是很稳定、很可靠,但是可靠到一定程度就变成了死板,会变成一个牢狱或者困住业务上所需的灵活性,因此常常需要很多层抽象,很多层胶水代码,代码就开始变得非常的晦涩,非常的难懂。动态语言则完全是相反的,所有东西都是从类型上来讲,以函数为例,灵活性已经足够了,但是通常我们写着写着就忘记数据长什么样子了,你可能今天写了一个函数说,输入一个函数的数据,然后过了一个星期之后,我已经完全忘记这个数据是什么东西了,因为生产环境里面,类型系统在没有编译器的帮助下,基本上都是一次性的,这个问题对于用户来说有相当大的困扰。

一直以来,这两派之间没有争出特别的高低,静态语言笑动态语言做不出大系统,动态语言笑静态语言写的太慢、废话太多,今天这个主题当然不可能解决这个纷争,但是希望通过 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

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

我本人很喜欢写这个,我觉得给函数加上类型非常过瘾,但是有问题,那有别的办法吗?有的,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.type vs Core.spec

写在最后

core.typed 和 core.spec 你推荐哪个?

我的脑子喜欢 core.spec,因为有前景。我的内心喜欢 core.typed,因为给东西加类型写起来真得很过瘾。


写《程序员修炼之路》的 Andy Hunt 和 David Thomas 大师曾说,要在软件开发这个行当立于不败之地,应该“每年学一种新的语言”。10 月 QCon 上海站上,C++ 之父 Bjarne Stroustrup 会分享关于 C++ 语言的发展和未来编程语言格局,还有摩根大通高级程序员赵劼(老赵)、阿里中心主管杨冠宝(孤尽)、PingCAP 首席架构师唐刘、饿了么资深 Android 工程师张涛、沪江资深 Android 工程师何梁伟、Movoto 前端工程师吴名扬等,分享有关 Kotlin、Rust、TypeScript、.Net 的语言实践,也欢迎你到现场和我们交流。

点击【阅读原文】抵达 QCon 全球软件开发大会,学习 2017 你想学习的新语言。如有任何问题可联系购票经理 Hanna ,电话:15110019061,微信:qcon-0410。


作者介绍

何婧誉(Loretta),Morgan Stanley VP。一枚剑桥大学计算机科学系毕业的妹子,兴趣范围从技术、数学、金融到桌游、国标、英文书法、语言学、哲学、钢琴等范围极广,属于样样都知道一些的典型 jack of all trades。技术上主要擅长 JVM 语言,有几年 Java 经验,2010 年遇见 Clojure 之后顿时被其简洁的语法、简单的写法及极具表达力的特性深深吸引,2011 年得以开始专业 Clojure 5 年多,现于大摩写 Scala。主要用 Clojure 做数据流处理,但也曾用其做过网络应用乃至安卓应用。JVM 之外亦与 Python、Perl 等主流语言,以及 ML 等非主流函数语言打过交道。约四年前开始与国内的 Clojure 社区有所接触,业余时间致力于解答 Clojure 相关问题,并希望能将 Clojure 的影响范围继续扩大。

今日荐文

点击下方图片即可阅读

百度正式开源其 RPC 框架 brpc



首页 - InfoQ 的更多文章: