正确的废话

所谓「废话」,大抵可以被分为两种:1)因为完全失却了逻辑,而导致没有任何理解的可能的话;2)因为陈述了过于明确的事实,而无仔细考虑的价值的话。后面这一种还有另外的一个名字「正确的废话」。倘若要一些学究气,或许可以叫他重言式(tautology)——然而我并不想在逻辑学的话题上作过多的探讨。

这篇文章,主要是想要讲述我近些日子学习程序设计而产生的一些古怪想法(或称「经验」「心得」之类。这些名词在我看来有些大而空,尤其不适用于我这类初学者,故不用)。这些想法,可能有些人赞同,而另外一些人反对,因此他们就不是些正确的废话。只是由于我的这些想法乍一看会感觉有些道理,却又没有实际表达任何有用的知识,看了之后对实际的学习与应用毫无用处,才取了一个接近这个概念的标题。所以这篇文章从一开始就文不对题。

需要注意的是,本文包含了一些看起来客观的论断,他们其实全部是笔者的主观意见。

# 函数式编程是一个营销词汇吗

雾雨魔理沙(M. Kirisame)曾经说过「函数式编程只是一个 marketing phrase。」不论是维基百科上的页面(强烈不建议将维基百科作为可靠的信息来源,尤其是时事和任何可能牵扯到政治问题的话题),或是我认识的任何一个人,都没有办法给出「函数式编程」的准确定义来。

两年前的我或许会认为函数式编程便是在 JavaScript 里多用一些 ES2015 标准中新增的数组方法。

//(这是函数式编程吗?)
let sum = 0;
let arr = [1, 2, 3];
// Xy Ren 2018-06-23 @ commit 3cbdfe:
// 使用函数式编程。把 for...of 替换为 Array.prototype.map。
arr.map(function (i) {
  sum += i;
});

对于这个问题本身,我不想做出评判,因为他涉及到某种信仰上的问题。同样的问题也适用于所谓的面向对象(object-oriented,或称物件导向)编程,以及命令式(imperative)编程。我们可以理所当然地声称这两个词也同函数式编程一般子虚乌有,因为事实上我们也没法找到对于他们的准确定义。

然而,他们并不是子虚乌有的。正如一谈到面向对象编程(对不起了,Smalltalk 用户们),我们就会想到入门教科书中试图对于「对象」和「类」为何概念作出的古怪的类比,诡异的抽象类与单继承,长得令人抓狂的方法名,以及 Java 中令人迷惑的几十上百种设计模式;而一谈到命令式编程,脑海里就会蹦出一堆细细碎碎的意大利面条,缓缓堆叠在主函数里形成一座令人作呕的小山——函数式编程肯定也有一些难以抓住的所谓「特征」,只在直觉和下意识中猛然闪现。

# 函数的纯粹性

其中一个要点在于函数的纯粹性(purity)。在程序设计中,一个纯粹的函数就仿佛一个数学中的函数,或称一个映射——把输入映射到输出,别无他用。如果一个函数有意地去更改了一份与他本身进行运算无关的内存(譬如,更改一个全局变量),或者进行了某种 I/O 操作,那么他就不是纯的。

函数的纯粹性会带来一些好处,譬如我们得以确定使用的函数不会突然进行不可预料的 I/O 操作(在 Haskell 中,有 I/O 操作便意味着你在函数类型签名上必定会看见 IO 二字);但更加重要的是,纯粹性让我们得以将数学意义上的函数与程序设计意义上的函数等同起来,进而方便地利用数学中的严谨结果,作用于实际的应用之中。

诚然,有许多语言并不完全是纯粹的,但我们仍然称他们为函数式编程语言(一些狂热者会拒绝称他们为函数式),如 OCaml、Scala 等。但要点并不是 Haskell 般的强制纯粹性——这偏执甚至将必要的副作用都化为一个宁愿表面纯粹但带有「魔法」的 RealWorld -> (a, RealWorld) 函数;而是我们要重视纯粹性的思想,适当地部分应用他。

# 「一等公民」?

所谓的函数为「一等公民」,其实是许多现代语言已经有了的特性。这个词的意思,无非是函数作为值而赋予变量或传递

Python 与 JavaScript 之流已经欣然同意将函数直接作为参数传递;C# 等语言也可以通过 delegate 而间接实现。只有 C++ 一类的古怪语言仍然要通过 std::function,函数指针和另外一切的奇怪方案而解决;假若你在错误的时间用了错误的方案,还会招致资深开发者的嘲讽。

所谓「一等公民」,是一个必要的先决条件——先于所谓的高阶函数与函数组合等等。若没有这「一等」,那在函数上的更多处理与抽象都极度困难乃至无从谈起。

# 柯里化

我曾经对柯里化究竟能不能派上用场感到疑惑;毕竟在以圆括号为调用标记的语言中,这种写法是非常吃力的(e.g. add(1)(2)(3)add(1, 2, 3) 相比)。然而,在 Haskell(或类似的)语言中,这个问题却被解决了,原因有二:1)函数调用以空格为标记;2)函数作为参数传递的广泛应用。

如果我们只给一个函数提供一部分参数,而留下另一部分俟其他时机补全,那么这个函数就被部分应用(partial applied)了;一些人可能会对此概念感到疑惑,因为他们根本没有做过这样的事情。诚然,部分应用并非一定是必要的;但他是有助于抽象的。

— This article is unfinished. —