写点什么

Clojure 太灵活,我们能如何驾驭它

2017 年 10 月 06 日

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

引言

这个题目非常的生动活泼,但是下面可能有点干(货)。上次来 Qcon 是两年前的事情了,在上海,那次我对 Qcon 是做了一个比较笼统的这样的一个介绍,所以这次主要来讨论一下动静态语言的问题,这个问题争议非常的大。

Morgan Stanley 公司在国内的技术圈可能不像在英美那边对投行特别有认同感,大摩的技术文化跟团队协作,跟我之前的四个东家相比的话,并没有差到哪里去的,而且他做得东西也非常有意思,大摩现在是全球前三大的 Scala 的公司,公司内部也做了很多编译系统的改进,我们大概有 250 万行 Scala 代码、三四百个 Scala 程序员,内部做得东西也是非常有趣,是做一个很纯函数的系统,是为了解决分布式计算不够简洁的问题,即内部的函数基本上全部都是纯的,基本上没有负作用,这样的话,在什么地方执行都是完全没有关系的,反正结果都是一样的,那么在这样的理念下面,你可以想象 250 万行 Scala,我们做出了双时效数据库,做出了很多支持函数理念的东西,这个平台理论上是非常先进的,所以我在大摩的感觉也是天天可以学到很多东西。

1. 静态语言 VS 动态语言

静态语言因为类型系统的关系,一直给人的是很稳定、很可靠,但是可靠到一定程度就变成了死板,会变成一个牢狱或者困住业务上所需的灵活性,因此常常需要很多层抽象,很多层胶水代码,代码就开始变得非常的晦涩,非常的难懂。动态语言则完全是相反的,所有东西都是从类型上来讲,以函数为例,灵活性已经足够了,但是通常我们写着写着就忘记数据长什么样子了,你可能今天写了一个函数说,输入一个函数的数据,然后过了一个星期之后,我已经完全忘记这个数据是什么东西了,因为生产环境里面,类型系统在没有编译器的帮助下,基本上都是一次性的,这个问题对于用户来说有相当大的困扰。一直以来,这两派之间没有争出特别的高低,静态语言笑动态语言做不出大系统,动态语言笑静态语言写的太慢、废话太多,今天这个主题当然不可能解决这个纷争,但是希望通过 Clojure 这个语言可以给大家一些不太为人知的思路。马上就有人来问了,我写 Clojure 就是为了逃避这样的内容来写系统,这样灵活多好用啊,我想写什么就写什么,快速原型靠的就是这个,我非常同意这一点。

2. 简单示例

在 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 作为一个动态语言的弊端,就是太灵活,导致经常会忘记这个函数的参数是长什么样子,而且这个是小项目,项目一大,那就更麻烦,那我知道你们有人会说的,说这个文档不就是做这个事情的吗?文档跟测试,但是文档它本身的代码是剥离的,它没有紧密的联合在一起,而相对代码本身是没有限制的,所以完全有可能你们自己有经验对吧?比如说很多代码上面会写,但是其实代码里面并没有,它可能起到的效果某种程度上也是挺有限的。

3. 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 进行类型推导)
  • 宏也会被展开后再推导类型

4. 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 支持更复杂的数据结构

5. Core.type vs Core.spec

总结

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

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

作者简介

作者:** 何婧誉(Loretta),**Morgan Stanley VP。我是一枚剑桥大学计算机科学系毕业的妹子,平时爱好各种新鲜事物,不会的都想学一点看一点。兴趣范围从技术、数学、金融到桌游、国标、英文书法、语言学、哲学、钢琴等范围极广,属于样样都知道一些的典型 jack of all trades。有收集德式桌游及大型乐高模型的癖好。

技术上主要擅长 JVM 语言,有几年 Java 经验,2010 年遇见 Clojure 之后顿时被其简洁的语法、简单的写法及极具表达力的特性深深吸引,2011 年得以开始专业 Clojure 5 年多,现于大摩写 Scala。主要用 Clojure 做数据流处理,但也曾用其做过网络应用乃至安卓应用。JVM 之外亦与 Python、Perl 等主流语言,以及 ML 等非主流函数语言打过交道。

约四年前开始与国内的 Clojure 社区有所接触,业余时间致力于解答 Clojure 相关问题,并希望能将 Clojure 的影响范围继续扩大。

2017 年 10 月 06 日 18:211946

评论

发布
暂无评论
发现更多内容

技术工作的一二三之价值观方法论

拖地先生

个人成长 方法论

[JVM] String#intern 面试必会

猴哥一一 cium

Java JVM string pool string Java 25 周年

JVM最佳学习笔记<三>---虚拟机性能监控与故障处理工具

Loubobooo

Java JVM

简述 HTTP 缓存相关的首部及其行为

黄耗子皮

缓存 HTTP

将footer固定在底部: Flexbox vs Grid

寇云

CSS css3

阅读对写作的好处

七镜花园-董一凡

写作

Python 沙盒环境配置

黄耗子皮

RocketMQ broker.properties

李绍俊

RocketMQ

JVM最佳学习笔记---总览

Loubobooo

Java JVM

Yii2.0 RESTful API 之速率限制

Middleware

php RESTful Yii2

一周信创舆情观察(5.18~5.24)

统小信uos

基础软件 操作系统

运维那点事 - jenkins流水线

yann [扬] :曹同学

JVM最佳学习笔记<一>---Java内存区域与内存溢出异常

Loubobooo

Java JVM

Yii2.0 RESTful API 之版本控制

Middleware

php RESTful Yii2

介绍一下自研开源NLP工具库---MYNLP

陈吉米

自然语言处理 中文分词 mynlp nlp

JUC整理笔记三之测试工具jcstress

JFound

Java

Java 异步编程:从 Future 到 Loom

理帆

Java 并发编程 kotlin Netty

企业也有中年危机?探讨数字化与永续经营

fino星君

数字化转型 小程序生态

到底谁是你老板

Neco.W

工作 创业心态

JVM最佳学习笔记<二>---垃圾收集器与内存分配策略

Loubobooo

Java JVM

ESP8266远程控制+MicroPython 固件初体验

黄耗子皮

物联网 esp8266

JVM最佳学习笔记<四>---虚拟机类加载机制

Loubobooo

Java JVM

技术工作的一二三之快餐

拖地先生

项目管理 软件开发 技术管理 软件开发流程

技术工作的一二三之内功

拖地先生

个人成长

【译】并不存在的普通用户(面向极端用户的设计)

Yukun

设计思维 可用性

钱从哪里来 - 中国家庭的财富方案

石云升

读书笔记 工作 财富 买房 资产配置

Yii2.0 RESTful API 基础配置教程

Middleware

php RESTful Yii2

最长回文算法(马拉车算法)分析

Gadzan

Java 算法 LeetCode

一个前端的 Windows10 开发环境

Gadzan

前端开发 windows Windows Terminal 环境安装 开发工具

运维与云

yann [扬] :曹同学

Yii2.0 RESTful API 认证教程

Middleware

php RESTful Yii2

演讲经验交流会|ArchSummit 上海站

演讲经验交流会|ArchSummit 上海站

Clojure太灵活,我们能如何驾驭它-InfoQ