函数式编程风格#

命令式和声明式(Imperative & Declarative)#

在谈论 FP 范式这个陌生词汇之前,我想先聊些我们熟悉的。

1990年后,C 作为计算机教育的101课程,逐渐在中国高校中普及开来。这是一门典型的命令式语言,程序员使用运算,循环和跳转语句,作为汇编语言的抽象对硬件发出指令。这种范式还有一个名字更加广为人知“面向过程编程”。

现今,几乎所有常见的编程语言都支持这一范式,包括 Java, Python 和 C++ 等常见的面向对象语言。面向对象要求将数据和操作数据的行为封装在对象中,通过这些对象来组织代码。逻辑的封装与数据的抽象并未改变人们依赖显式控制流去操控程序的状态以实现功能。

不同于命令式的"你该那么做",声明式语言更关注“你该做什么”。

请允许我用 C# 举个例子:

(string name, int age)[] people = new[]
{
    ("Alice", 12),
    ("Bob", 5),
    ("Charlie", 35),
    ("David", 65),
    ("Eve", 40),
};

我们需要在人群中筛选出成年者,并保存他们的姓名。

这是一个常见的命令式实现

string[] adults = new string[people.Length];
for(int i = 0; i < people.Length; i++)
{
    if (people[i].age >= 18)
    {
        adults[i] = people[i].name;
    }
}

而声明式则是这样的写法

string[] adults = people
    .Where(p => p.age >= 18)
    .Select(p => p.name)
    .ToArray();

令人遗憾的是,虽然论述到本节时,一定会有读者疑惑于“命令式语言中的方法和声明式的‘做什么’有什么区别”,但鲜见讨论函数式编程的书对此做过解释。

让我们再来回顾命令式和声明式所关注的点。

  1. 命令式是基于计算机硬件接收并执行指令的层层抽象,关心的是如何逐步发送指令完成对程序的构造。
  2. 声明式则最初被定义为非命令式,人们不关心功能如何实现,而是关心功能是什么。这种思潮衍生的极致便是 DSL(领域特定语言)。

倘若存在一个神奇的编译器,可以编译人类日常交流的自然语言。那么,你对他说“嘿!哥们。请先把标准输入流中的第一个和第二个数存起来,然后把他们加起来并编辑成音频,最后向外播放这个音频”,何尝不也是一种命令式呢。

如果你需要的是声明式语言,请这么说“我需要一个软件,它可以接收用户输入的前两个数,然后用音频输出他们的和,今天下午给我”。笑)

现在,问题的答案已经呼之欲出了。命令式语言中的方法仍然是指令的体现,无论其是否由你实现。命令式关心的是实现的细节,而声明式关心的是需要实现什么。

也正如此,两种范式并不于客观上泾渭分明,现在越来越多的编程语言都朝着混合范式的方向发展,我们所使用的 C# 正是其中的佼佼者。

相同的设计原则#

或许你已经为之发愁过了,命令式程序员并非未曾意识到状态突变带来的危险,他们会使用锁,或是控制尽可能少的状态更新入口来维持程序的稳定运行。同样的,他们也会使用事件和回调来优化流程控制。

我说这些并非期望给读者带来“命令式没有函数式编程靠谱”的想法,恰恰相反,我想强调的是:无论哪种范式,在构建或大或小的精妙系统时,我们都遵循着同样的设计原则。

或许你听过诸如单一职责(single responsibility),高内聚低耦合(loose coupling high cohesion),DRY(don’t repeat yourself)一类的词汇,我们不会在这里展开他们,只是希望至少这些词汇能给你带来些安心感。

什么是函数式编程#

函数式编程中,程序的构建基于 lambda 演算的组合,应用和归约。

想象一个只有两侧开口的盒子,你向一侧放入些值,然后可以在另一侧取出一个新值,在此过程中,你并不知道这个盒子是如何运作的,好在你也并不需要知道。这指向了函数式编程中的一个特性:不可变性。确切的说,lambda 演算中没有状态,每一次从盒子里取出的东西都是全新的。不过作为图灵完备者,FP 也可以利用 lambda 演算去模拟状态, 我们会在有状态计算一章中去详细介绍。

还是那个盒子,这次你没有给它塞值,而是将另一个盒子塞了进去,令人惊奇的是,另一端出现了一个全新的盒子。我们通常管这种可以产生盒子的盒子叫做高阶函数

就这样,你可以把这些盒子和值层层嵌套,最后你会得到一个终极的盒子,他就是你所期望构造的程序。当然,因为他本质上仍然是个盒子——我们现在开始叫他函数吧,所以你可以把它当作一个即插即用的组件,插入到任何一个符合规范的另一个函数(程序)中。

如果你这么做了,恭喜你,你已经开始实践函数式编程了。在使用函数设计精妙的应用程序之前,我们还需要掌握一些基础知识。

comments powered by Disqus