Rich Hickey - Effective Programs
Table of Contents
概述
Rich Hickey 的演讲很有意思,说的观点也总是很有说服力,且很实际。这里并没有做全文翻译,甚至说部分翻译都不是。
感兴趣的人应该去听原文 https://www.youtube.com/watch?v=2V1FtfBDsLU&t=3964s,也可以去看下别人做的演讲的 transcript.
这篇演讲中,Rich Hickey 从自己的编程生涯经验讲起,总结了他关于编程中可能存在的各类问题,以及这些问题的主观占比。然后描述了 Clojure 重点考虑解决的是哪些部分,同时依次解释了对应的处理方式。
Effective programming
Rich Hickey 列举了自己的软件生涯写过的程序,讲述了自己和一个计算机语言小组打交道的一段经验,给出了他对于 effective programming 的理解。
有一种说法,"Clojure is opinionated"。对于这个说法,有两方面问题比较有意思:
- Clojure 在那些方面是 opinionated?
- 说一门语言是 opinionated,到底是指什么?
对于 Clojure,更好的说法可能是,它强力支持了一些风格。所以在这种程度上,也许你可以说它是 "opinionated",不过设计就是关于做出选择,而 Clojure 的设计过程中,很大的选择就是决定不引入什么,演讲后续也描述了这些。
至于这些设计偏好哪来的,Rich Hickey 说,他作为一个人,当然是有所偏好,而这些源于他的经验:
- Scheduling system (C++)
- Broadcast automation (C++, Java)
- Audio fingerprinting and recognition (C++)
- Yield management (Common Lisp producing SQL)
- More scheduling (Common Lisp -> C++)
- US national exit poll, election projection system (C#)
- Clojure (Java, Clojure)
- Machine listening, artificial cochlea (Common Lisp, Mathematica, C++, Clojure)
- Datomic database (Clojure)
几乎所有这些项目都涉及到数据库,各种各样的数据库,从 ISAM 数据库,到 SQL,到对于 RDF 的尝试。
Rich Hickey 说,他有一段时间经常去 Light-Weight Languages 研讨会,里面有各种计算机语言的 geek,有两个家伙,都是计算机语言的研究人员,一次,其中一个人对另一个人说 “David,你上次用数据库是什么时候?”,另一个人回答 “我从来没用过数据库”。
于是他决定写 Clojure,因为,如果一个不用数据库的人都能写编程语言,/任何人都行/。
有很多类型的程序,有一类程序 Rich Hickey 给起了一个名字,"situated" (位于某地的) 程序。通俗说,就是你可以看出这些程序是在其所在世界的某地的,而且跟它所在的世界有很强的连接。它们有很多特征:
- 一个特征是它们会持续执行。不是说提交一个计算,然后它运行一会给一个结果,跟 AWS lambda 函数那样;而是,它们会持续不停的执行,24/7 的执行。
- 它们几乎总是用于处理信息。这些系统*消费*信息,这对它们必不可少;其中一些也会产出信息;而且它们会记录它们做的事情。
- 而且它们需要有时间跨度极大的记忆。前面说过,他做的所有项目几乎都涉及数据库。
- 这些程序需要应对真实世界的不规律。来自真实世界的需求总是千奇百怪,前一秒定的逻辑,随时可能被一条例外破坏。
- 这类程序几乎总是需要和其它系统打交道,很少只是运行在它独立的宇宙里。
- 除了和其它系统打交道,它们还要和人打交道
- 它们所在的世界里的东西会发生变化,其所在世界本身的规则可能发生变化。
- 另外,这些程序还坐落于某个软件环境、社区中。你的程序不太可能从零开始。
什么是 effective program?
Effective programs,effective 指的是产出期望的结果。与之相对,Rich Hickey 说,它听到正确的程序就头疼,正确指什么,让 type checker 高兴么?
另一方面,他也不希望被曲解成,放弃思考,能用就行。所以能用(works)究竟指什么呢?能用应该指完成了特定工作,且产出了期望的结果。
什么是编程的目的?
编程这件事到底是关于什么的?Rich Hickey 说,对于他,“编程就是让计算机在现实世界中 effective”。计算机如何在现实世界 be effective 和人在现实世界中如何 be effective 其实一样。正如一个人可以通过帮助另一个人 be effective 而使自己 effective,计算机也可以通过辅助人达到这点。
那么,我们作为人是如何 be effective 的呢?有时,是因为我们能精确的算出一些东西,诸如导弹轨迹。但是大多时候不是。 Rich Hickey 总结,“being effective 大多时候并不关乎计算,而是关乎从已有的信息中总结得到预测能力”.
这里的信息,是关于事实(facts),关于那些已经发生了的事情。经验,特别是编程世界中的经验,等同于信息,等同于已经发生了的事情。
什么不是编程的目的?
对于 Rich Hickey,“编程重要的不是编程本身”。编程不是关于证明程序类型与你主张的类型一致,证明这件事只跟证明这件事本身有关;他说,在他的整个职业生涯中,他的工作都在于编程所要实现的跟现实世界联系的那部分事情。
说这些并不是针对算法与计算。它们很重要,但是它们只是我们要做的事情的子集。即使像定理证明器、编译器这样的软件,也需要从磁盘读取需要的信息,或者吐出一些结果。毫无疑问,大部分的程序都是关于信息处理的。也不全是,当程序有 UI 的话,Rich Hickey 张开手,划拉一个大圈,那么程序中非 UI 的部分就只能占其中一个点了。
你需要使用各种库,当你开始这样做,程序中将引入不由你定义的东西;我们需要通过某种协议和这些库通讯。
在 situated 程序中,很难避免使用数据库;对于库的使用可能还能共享语言、JVM 或者说运行时,有了数据库,这点也将被打破,于是,我们开始面对跨语言,内存也不再共享。
这还没结束,程序可能与其它程序交互;这些程序可能用其它语言写就,这些语言都由自己一套关于逻辑应该怎样写就的想法。对于数据库,提供商给我们定义了 wire protocol,对于其它语言写就的系统,我们得自己定义一套协议,比如,JSON。
还没完,现实世界中,这整个构建的系统不太可能是一次性的,做完就完了。系统中的任何部分都可能发生改变。规则、需求、网络、算力,你使用的库本身,甚至你与库交互的协议,全都可能发生改变。
而 effective programming 就是关于如何处理所有这些事情。
Clojure 的设计目标就是应对由信息驱动的这些 situated 程序。当你目标不同,比如你想针对定理证明或者编译器去创建一门语言,它肯定和 Clojure 不太一样;当你选择语言时,想清楚,这些语言为什么而设计。
编程中存在哪些问题
下图 Rich Hickey 根据自己的经验总结了编程中存在的问题,同时,绿色的部分标出了 Clojure 试图解决的部分。
Figure 1: The problems of Programming
关于资源利用,Clojure 没有做额外的事情,直接沿用了 Java 的运行时。
Clojure 设计目标
Clojure 设计时主要的目标,或者希望处理的问题是:
- 我们能否通过明显更为简单的东西,构建 effective, situated 程序?
- 来自编程语言层面的认知负荷非常小
- 当然,还有做一个可以日常使用,能替代 Java/C# 的 Lisp
构建一门实用的语言,还要考虑一些元问题:
-
可接受性
- 性能
- 支持的平台
-
提供的能力
- 杠杆
- 兼容性
还有些不应该成为问题的东西
- Lisp 语法(那些括号)
- 动态类型
记住,Clojure 的目标是服务于之前图里的那些占据主导的问题。
PLOP - Place Oriented Programming
所谓 place oriented(“面向位置”),指的是很多传统语言中编码方式,即你定义某个变量,很可能直接对应某个内存位置里的某个值,然后通过改变这些位置的值来实现所需的逻辑。
拥抱值语义(value semantics),使用不可变(immutable)数据结构能解决大部分 PLOP 风格带来的问题。
于是,头号问题就是就是找到一个高效的 immutable 数据结构。这也是 Clojure 背后最主要的研究工作,从 Okasaki 找到 Bagwell,最终实现了高效的 persistent data structure.
Information
Rich Hickey 提到,静态语言最让他不爽的点就是,它们对于信息的处理很糟糕。
信息是什么?信息,内在地,很稀疏。它是你知道的东西,是世界上在发生的事情。
你能知道什么?很难回答,时间往前,你会知道更多东西,会发生更多的事情,出现更多事实,信息不断累积,一刻不停的生长。
关于信息,我们还知道什么?事实上,除了名字,没有更好的方式与它们打交道。处理与人相关的信息时,名字极为重要,如果你只是说 “47”,没人知道它指什么。
还有一件事,Rich Hickey 说,他经常挣扎着要处理的。考虑有一个系统,我们针对一份数据建了一个类或者类型,然后某个地方,关于这份数据了解了一点额外的知识,该怎么办?重新定义一个和之前那个类型大部分一样的类型?如果支持派生/继承,派生出一个类?考虑,现在你在另一个上下文中,你知道一点这个,知道一点那个,如何建模这种情况?系统构建过程中,这类问题将不断积累。
所以,何种编码方式和信息最相容呢?我们将信息的容器作为其语义的驱动。我们会说“有一个,人有名字,有邮箱,有社保号”,这三个东西本身没有语义,但是这里作为这三个信息的那个容器有,而它们在这个容器的上下文中也有了语义。
但是,这不应该是这样的,你填写一个表单,你写进去的东西的语义不应该由这个表单主导,你只不过正好把这些信息填在这个特定的表单,你可能填在任何地方。表单是个信息搜集的装置,不是一个语义装置。类、类型与表单无异,通过它们,你为信息创建了一大批非常特定的类型(concretions1)。
编程文化中,抽象一般用作两种情况。一种是,“为某个抽象的事务命名”。Rich Hickey 说,很多时候我们给出命名,但作出的不是抽象,而是创建了非常特定的类型。关系代数是数据抽象,Datalog 是数据抽象,RDF 是数据抽象;但是你创建的 person 类,你的 product 类,这些不是数据抽象,这些是实现细节。
在 Clojure 里,我们鼓励使用 map,有巨量的库用于支撑对于 map 的使用。提取信息、合并信息,非常简单。
在 Clojure 里,名字是一等公民。 keyword
和
symbol
都是函数,它们能作用于 map
这种关联数据结构,它们知道怎么在这种关联数据结构查找自己。
关于 Clojure,还有一件很具潜力的事情,我们将语义和属性关联,而不是和什么聚合物关联。
Brittleness/coupling
关于系统的脆弱性和耦合程度,Rich Hickey 说,从他个人经验,类型系统将产出更为耦合的系统。
参数列表很不符合直觉,现实世界里,我们设计表单让人填填写信息时,会在旁边添加标签。当然,惯例,我们也会参数列表的参数起名字,但是很可能比较随意,当我们确实需要复用参数类型时,那么,可能你又需要定义这个非常具体的类型。
Clojure 是动态类型,我们使用 map 这种开放的数据结构,我们学着去只关心我们想要关心的部分,我们只处理我们想要处理的那些东西,或者说基于我们对于它的知识去处理它,然后不去动其它部分;我需要处理社保号,我知道给我的信息里包含社保号,那就处理它,至于你是否给了我更多东西,比如这个人有一辆车,那些我不关心,我便不动它。是的,我们失去了类型系统的保护,但是类型系统到底在保护什么呢?你的数据是不可变的,没人能动你的东西,所以需要保护什么呢?
语言模型的复杂度
C++ 是一门复杂的语言,Haskell 也是,甚至 Java。而 Clojure 很小,可能不如 Scheme 那么小,但是相比其它语言,确实小很多,它不过是基础的 Lambda calculus 加上 immutable functional core.
我们有函数,有值,你可以传值调用函数,然后得到一些新的值。就这些了。
不过执行模型有点麻烦,总之,作为 Java 的一个库,执行模型和它一致,如果发现性能存在问题,怪 Java. 不过,也意味着 Java 世界中所有用于提升性能的技巧和工具,也适用于 Clojure.
Parochialism (狭隘主义)
Wiki 中关于 Parochialism 的定义,关注问题的很小一部分而忽略其所在的全局。
这里,Rich Hickey 谈到类型,他说,类型是他真心不喜欢的东西,因此没有在 Clojure 引入。他对类型起的名字便是 "parochialism". 他讨厌这种想法 “我有个语言,非常酷,你应该这样去思考。你应该使用代数类型思考问题。或者说你应该用继承的方式思考问题”.
这些想法促进了强烈的狭隘主义。你开始用一种只在这门语言里面适用的规则建模事物,表示信息。
Rich 认为 RDF (W3C Resource Description Framework) 做了些正确的事情。因为它的主要目标是希望能合并不同来源的数据,而不让 schema 主导语义。
考虑两家公司合并成一家了,每家公司都有自己的数据库,可能都由自己的用户表,你的邮箱对应的那个人在都是你,但是两家系统不知道,所以需要通过某种方式去判定哪些信息事实是同一信息。怎么做呢,一种常见做法,是再引入一个数据库,一般来说是一个 RDF 数据库,用它来做信息的合并,用来定义哪些信息事实是同一份。
类型系统产生的问题和这个很类似。你会遇到
Ctx1PersonType
和
Ctx2PersonType
的问题,一份信息的语义由它的类型或者其容器(比如类)决定。
Clojure 中,我们提供了名字的抽象,通过 keyword
和
~symbol~,而且它们是可以有 namespace
的。你可以直接用名字进行语义的定义。这和 RDF 中 URI
作为名字的想法类似。
Distribution
互联网擅长 plain 数据的传输,其它试图抗拒这个方向的技术看上去都失败了。比如分布式对象。
Clojure 的子集 edn,就是单纯的数据,而且包含了这门语言大部分常见的数据结构。
运行时的可触达 (Runtime tangibility)
Rich Hickey 说,他在编程生涯相对较晚才开始接触 Common Lisp,这门语言让他最为兴奋的是其运行时的可触达性,它有非常具化的环境,你能在运行时见到各种名字对应的运行时实体,你能通过名字找到对应代码,你可以在运行时加载代码,你可以按需加载你要的代码到运行时。
Clojure 也给你提供了这些能力。
Concurrency
基本上,如果你有了不可变数据结构,并发的问题已经解决了大半。
Footnotes:
The Dependency inversion principle: "Depend upon abstractions, [not] concretions." https://en.wikipedia.org/wiki/SOLID