第01章:何去何从?
引言
大家好!我是富兰克林·弗里斯比教授。很高兴与各位相识。在接下来的时光里,我将教授函数式编程的基础知识。先不讨论我的经历,请问您是否对JavaScript语言有所了解?是否具备些许面向对象编程经验?是否自视为职场程序员?您无需拥有昆虫学博士学位,只需掌握基本的代码调试能力。
我不预设您具备任何函数式编程基础,毕竟妄下假设往往适得其反。但我希望您曾遇到过可变状态、无约束副作用和非理性设计带来的技术困境。既然彼此已认识,让我们正式启程。
本章旨在帮助您体会函数式程序的核心追求。为理解后续章节,我们首先要明确函数式程序的界定标准。否则我们将陷入盲目躲避对象的低效编程——如同在代码迷宫中徒劳涂鸦。我们需要清晰的目标指引,就像在惊涛骇浪中手握星象罗盘。
目前存在若干通用编程原则——各种首字母缩写信条指引我们穿越应用开发的黑暗森林:DRY(不重复逻辑)、YAGNI(不需要的就别做)、低耦合高内聚、最小惊讶原则、单一职责原则等等。
我不准备罗列所有耳熟能详的编程准则……关键在于这些原则在函数式场景中同样适用,但它们只是通向终极目标的支线任务。在继续前行的这一刻,让我们重点体会手指敲击键盘时的精神内核——函数式编程的应许之地。
初探端倪
让我们从一段反例代码开始。这是一个海鸥种群模拟程序:当鸟群合并时会形成更大群体,繁殖时会以交配对象数量为基数增加成员数。注意这段代码并非优秀的面向对象实践,其设计专门用于揭示现代编程中基于赋值的缺陷模式。请看:
class Flock {
constructor(n) {
this.seagulls = n;
}
conjoin(other) {
this.seagulls += other.seagulls;
return this;
}
breed(other) {
this.seagulls = this.seagulls * other.seagulls;
return this;
}
}
const flockA = new Flock(4);
const flockB = new Flock(2);
const flockC = new Flock(0);
const result = flockA
.conjoin(flockC)
.breed(flockB)
.conjoin(flockA.breed(flockB))
.seagulls;
// 32谁会编写如此骇人的代码?(原注:此处示例展示了错误的可变状态处理)其内部状态的频繁变更令人难以追踪。更糟糕的是,计算结果竟然出错!正确值应为「16」,但在计算过程中「flockA」的数值被永久篡改。可怜的「flockA」!这是信息技术领域的无政府状态!这就是毫无节制的野生动物算术!
如果您无法理解这段程序,这很正常——因为我同样难以理解。需要记住的核心要点是:即使在这样的微型示例中,状态与可变值的跟踪已然十分困难。
让我们用函数式方法重新实现:
const conjoin = (flockX, flockY) => flockX + flockY;
const breed = (flockX, flockY) => flockX * flockY;
const flockA = 4;
const flockB = 2;
const flockC = 0;
const result =
conjoin(breed(flockB, conjoin(flockA, flockC)), breed(flockA, flockB));
// 16本次计算结果正确,且代码量显著减少。虽然函数嵌套略显复杂(我们将在第5章解决此问题),但已有提升。再深入分析:明确函数本质至关重要。若仔细审视定制函数,会发现其实仅涉及基础加法(「conjoin」)与乘法(「breed」)。
这两个函数除了命名并无特别之处。让我们将函数重命名为「multiply」和「add」以揭示其本质:
const add = (x, y) => x + y;
const multiply = (x, y) => x * y;
const flockA = 4;
const flockB = 2;
const flockC = 0;
const result =
add(multiply(flockB, add(flockA, flockC)), multiply(flockA, flockB));
// 16此举让我们继承了数学先贤的智慧成果:
// associative
add(add(x, y), z) === add(x, add(y, z));
// commutative
add(x, y) === add(y, x);
// identity
add(x, 0) === x;
// distributive
multiply(x, add(y,z)) === add(multiply(x, y), multiply(x, z));呵,这些经典数学定律终将派上用场。不必担心定律名称生疏——对多数人而言,自上次学习算术定律已过去许久。现在尝试用这些定律简化海鸥种群程序:
// Original line
add(multiply(flockB, add(flockA, flockC)), multiply(flockA, flockB));
// Apply the identity property to remove the extra add
// (add(flockA, flockC) == flockA)
add(multiply(flockB, flockA), multiply(flockA, flockB));
// Apply distributive property to achieve our result
multiply(flockB, add(flockA, flockA));杰出!我们无需编写任何自定义代码(除调用函数外)。虽然此处列出「add」和「multiply」定义仅为保持代码完整,但实际上根本无需手写——任何第三方库都会提供现成的加减乘除函数。
或许您会质疑「举这样的数学示例太经不起推敲」,或「真实程序不可能如此简单并据此推演」。选择此例正因为加减乘除为人所熟知,便于展现数学对我们的实践价值。
不必沮丧——本书将循序渐进融入范畴论、集合论和λ演算,编写具备同等优雅性的真实案例。您也无需成为数学家,学习过程将如使用「常规」框架或API般自然流畅。
您或许会惊讶地发现:我们可以参照上述函数式范例编写完整应用程序。这样的程序兼具健壮性、简洁性与可推理性,且无需重复造轮子。数学定律如同程序员的法典——本书倡导严守理性法则,正如违法行为对罪犯是优势,对工程却是灾难。
我们需要理论的支撑——其中每个组件都能严丝合缝地协作。要将具体问题抽象为通用的可组合单元,继而利用其数学特性实现目标。这比「随心所欲」的命令式编程需要更多自律(后续章节将明确定义「命令式」,目前可理解为非函数式编程)。在原则性数学框架下工作带来的技术红利必将令您惊叹。
我们已窥见函数式北极星的微光,但要真正启程,仍需掌握若干核心概念。