居然顶到了某种字数上限,分两段发 …
首先我无法理解主为什么会有在这个社区发这个帖子的想法 …
虽然是 F2EX,但是我并不期待这里的人对所谓 FP 能有什么高见,至少在这里的人对 FP 有个哪怕是最基础的认识之前
说实话,哪怕是在 HN 上,这种帖子也是引战贴(并且随便一搜就能在同一个站点找出十个战场来),还是去 LtU 抱团取暖吧 …
关于“FP 概念在大众已经流行起来了”,知乎上有相关问题: https://www.zhihu.com/question/30190384 为什么这两年函数式编程又火起来了?
主要想讨论“函数式编程语言的未来”,首先要解决“函数式编程是什么”的问题。这在任何一个主题和函数式编程无直接关系的社区中都是一个无法解决的问题。就连 JavaScript 和 Ruby 这种社区中也免不了三天两头就有人出来科普什么“函数式思维”,说明就算这些一只脚在 FP 里面的语言也没法解决这种问题 …
我水平有限,不敢为“函数式编程”这个词给出一个足够“函数式”的定义(“函数式编程”这个词背后的 implication 太多了)。但是在我这几年折腾函数式编程的过程中,有那么两点最大的感受:
第一是对于任何 non-trivial 的程序而言,最重要的东西之一是管理复杂度( Complexity )。管理复杂度最重要的手段是模块化,或者按照 FP 的黑话,叫组合( Composition/Composable/Composability )。而这也是 FP 最看重的东西之一。
Composition 是什么意思?就是看上去非常牛逼如同魔法的东西,一点点拆开发现是由非常简单的东西构成的。比如乐高方块组合成完整的乐高玩具,细胞组成器官,器官组成人体,晶体管组合成逻辑门再组合成芯片,if、for 等操作组合成函数,函数再组合成程序,若干个程序模块组合成一个完整的软件系统。比如说人体一定程度上是 composable 的,因为器官可以移植,器官移植需要配型,所以器官又不那么 composable。
在编程语言中的例子比如:C 的宏有不 Composable 的方面(比如使用有副作用的片段展开可能会导致副作用执行多次); goto 不如 if 和 for 更 composable:用 if 和 for 写的程序可以随便拷贝到其他地方,作用还是一样的(在预期之中),但是纯 goto 写的做不到; Checked Exception 的问题也是出在不 Composable 上——使用其他模块除了需要了解模块本身接口之外,还需要折腾模块本身,以及可能的底层模块暴露出的异常;多重继承会出现 Diamond Problem,这意味着在使用多重继承时必须挨个确定每个基类都有哪些基类,也不 Composable。Implicit Coercion 看上去允许不同类型的值之间 compose,但是往往也会伤害 composability:比如 C 中无符号和有符号值之间的比较。锁也是不 Composable 的(见 https://en.wikipedia.org/wiki/Lock_(computer_science)#Lack_of_composability )。JS 中的 callback 相比 Promise 的缺点也是不 Composable:值是编程语言中最 Composable 的东西,并且 Promise 的错误处理也更有利于 compose。
可以发现不 Composable 的地方往往体现为各种各样的“坑”,而 Composable 的地方我们一般都 习以为常。
对于管理复杂度这一问题(比如设计软件),Composition 提供了极其明显的好处:在理想情况下,系统可以被分解为若干模块,模块可以递归地分解为更小的模块。特定的开发者在特定的时刻只需要关心自己正在开发的模块,以及所依赖的模块的接口。模块可以独立地开发与测试,模块之间的依赖被最小化,实现细节和复杂性被隐藏在抽象接口之后。
较差的 Composability 对于简单程序来讲问题不明显,项目越大,造成的麻烦就越大。
函数式编程提供了实现 Composability,管理复杂度的理论。函数式编程提倡通过小函数组合成更大的函数的方式来构建软件,对于 Composability 极为重视(某些 FP 厨经常扯的猫论更是从根上就是关于 Composability 的理论)。
函数式编程同时认为 Side Effect 是 Composability 的最大障碍,虽然“函数式”这个词可以被套在很多东西上,但是 Side Effect 不是其中之一。如果以 Side Effectful 的方式写程序,就意味着当使用某一模块时,必须考虑这个模块是怎么实现的,可能触发哪些 Side Effect,这个模块依赖的模块又可能触发哪些 Side Effect,这些被依赖的模块依赖的模块又可能触发哪些 Side Effect …
但函数式编程并非完全拒绝 Side Effect (这不可行),而是强调应该 管理 Side Effect。比如 Haskell 强制使用类型的方式标记哪些函数有 Side Effect,哪些函数没有,有 Side Effect 的函数可能会包含哪些 Side Effect。这就强制你把有 Side Effect 和没有 Side Effect 的函数分开,并且对无 Side Effect 的函数实现了 Composable 的目标。
ML、LISP 和 Scala 等则对程序员比较信任,除了变量赋值稍微麻烦一点,使用 Side Effect 和 Imperative 语言没有什么不同。但从语言设计到库再到代码风格都偏向于限制 Side Effect 的范围。
—
第二是对于简洁、优雅的理论框架的追求。(注意这里的“优雅”不是语法上的“优雅”)
什么叫做“简洁、优雅的理论框架”呢?我举一个和目前 V 红话题相关,F2EX 应该能懂的例子:
https://raphlinus.github.io/ui/druid/2019/11/22/reactive-ui.html 这是我前段时间研究 GUI 时找到的一篇文章。文章试图用一个“简洁、优雅的理论框架”,统一目前流行的 React、Flutter 等 GUI 框架的思想——注意意思不是统一这些框架,而是尝试找出这些框架实现思想的共同点,并总结起来。注意文章作者自己就是 Berkeley 的 PhD,同时自己也在开发一个 GUI 框架。
该文章提出:
* 所有的 “reactive” 框架(该文章并未明确定义 “reactive” 这个词,只是用它来概括现在流行的 UI 框架)都可以被视为一个 Tree Transformation 的 Pipeline ——从状态的 tree,生成高层的组件 tree,再生成底层的元素 tree,布局、渲染后变成屏幕上的像素。
* Tree 有两种表示形式,一种是 linked data structure (就像数据结构课本上教的二叉树),另一种是 “trace of execution”,即遍历 tree 得到的 flatten 表示。
* 不同的 GUI 框架在实现这个 pipeline 的很多细节有不同。其中比较重要的部分之一是当输入(状态 tree )变化之后,如何高效更新 pipeline 中的其他 tree——也就是 React 中的 Reconciliation,Angular 的 Dirty Checking 之类的。
这个“理论”最牛逼的地方在于它不仅统一了 React 和 Flutter 之类所谓“声明式”( whatever it’s called …)框架,还通过不同的“Tree 表示形式”,把渲染中间层和 imgui 这种 Immediate 模式也囊括进来,因此说是“Unified Theory”。这个文章是所有关于 GUI 的资料中,对我最有帮助的。
另外作为被王垠在去年 12 月 24 日喷过的人,我发现这套理论与现代优化编译器的设计思想有非常相似的地方。另一个 GUI 框架的开发者在看这篇文章时也有同样的感受: https://blog.anp.lol/rust/moxie-intro,并且他还找出了其他领域的例子。这时候直接搞到我颅内高潮了 …
其他相关的例子包括:
* 天文学在牛顿之前一直在试图解释天体的运行规律。
* 后来的物理学两百年来一直在追求把四种基本作用统一的理论。
* 生物学使用演化论解释不同生物之间不同特征的关系。
* 图形学使用 Path Tracing 渲染所有光学效果。
* 大数据处理基于 MapReduce 模型操作数据(嘛这其实是 FP 的东西 …)。
* 编译优化领域使用 Polyhedral Model 建模程序的行为。(编译优化好像还不存在所谓“Unified Theory” … 这是让我感觉很别扭的一点)
UNIX 的“一切皆文件”,“一切皆文本”和某些 OOP 语言中的“一切皆对象”其实同样也是非常熟悉的例子。
与之相对的是,很多编程语言的基础是很随意的:C 最开始是为了折腾 UNIX 整出来的,C++ 则是在 C 的基础上的另一个个人项目。JavaScript 是为了跟 Java 的风十天写出来的。PHP 是为了维护个人网站写出来的。Python 的性质也类似。
Java 反倒是最根正苗红的一个——Java 有 Guy Steele ( Scheme 设计者之一)的参与,泛型等则更有 Martin Odersky ( Scala 之父)和 Philip Wadler ( Haskell 设计者之一,同时也在所谓 “Monad” 概念的应用中有重要贡献)的参与。Pascal 同理。
基础“随意”是什么意思?意思就是语言的设计只是为了满足设计者当下的需求,缺少理论的指导。当然一门语言的设计一定有“为了满足某种需求”的目的,但是“我想做个语言来写博客”和“我们实验室想开发个语言来写 Theorem Prover”显然是两种目标。而这些“随意”的语言的设计者,在设计语言的时候,往往对“他眼前所要解决的问题”比对“程序语言设计”这件事要在行的多,换句话说“随意”的语言是由 PL 外行人做出来的 … C 的成功是 UNIX 的成功,C++ 的成功则是建立在 C 的成功的基础上,PHP 和 JavaScript 的成功是 Web 的成功——它们都不是“语言本身”的成功,而是傍着别人的大腿获得成功的。
理论的指导能带来什么好处?
相对论预言了黑洞,量子力学在半导体上有很多应用,真的有了物理上的统一理论,指不定又会捣鼓出什么东西。
写算法,了解了贪心、动态规划、分治等套路,能帮你更好地理解与设计算法(虽然有很多题还是做不出来 … 但是如果你不知道这些套路,只会更难)。
在编程语言中,循环可以被看做是递归的一种特例,那么可以把循环拿掉,用递归(以及 PTC )来实现循环。
Exception、Coroutine、Generator、Async/Await 等则可以统一在 Continuation 的概念下,那就只需要实现 Composable 的 First-class Continuation 就行了。
HM 类型系统则可以进行完整的 Type Inference,允许以动态语言的方式写静态语言——整个程序不需要写一个类型,却依然是类型安全的。
函数式语言则会如此设计:首先把 Lambda Calculus 拿来,Lambda Calculus 允许用非常简单的模型,表达足够的抽象。然后使用 Type 来扩展(限制)它,变成 STLC,再加入一些简单的扩展,比如 Parametric Polymorphism,Algebraic Data Type。最后把 int、bool 等 primitive type 放进去。基本上就是一个能用的语言。
整个语言核心就是这个样子,找不到多余的东西。每一个新的 feature 都是对之前语言非常自然的扩展。
再加入 Reference 用来处理 Side Effect,Modules 来组织程序,做个 Type Inference,这就是一个 ML 语言。
最重要的是,所有的东西都是 Composable 的。
当然 ML 自己有不那么 Composable 的东西,比如只支持 Rank-1 Type,可以说是为了 Type Inference 做的妥协。那也可以扩展到 Higher-Rank 和 Impredicative 到完全的 System F (只是这时候理论会告诉你 Type Inference 不好做了)。所有这些并没有限制程序员的能力,而是提供了更好的 Composability。
Side Effect 也不利于 Composability,这方面也有若干方法来管理,同样是为了更好地 compose。
所以说我对一个”函数式语言“的判定最重要的是它有没有建立在函数式理论的基础上。而至于什么乱七八糟的”First-Class Function“”Algebraic Data Type“和”Immutability“之类的具体的特性并不是最关键的,因为它们都是在这个理论上自然的延伸。
另外,我个人并不喜欢 FP 标榜“利于并发”来搞宣传工作,“利于并发”只是 FP 所提供的 Composability 的一个 Side Effect,而 Composability 本身是 FP 理论的一个 Side Effect。冲着“利于并发”折腾 FP 的,大概率会 totally miss the point。