为什么是函数式程序设计?

这是一篇有点邪性的文章,主要内容是传 FP 的教,当一个 QB 式的人物,把天真无邪的新人程序员带进 FP 的世界,变成 FP 魔法少女。

如果你不想当函数式魔法少女,那么请你立即退出这篇文章。

引言

函数式程序设计是一种具有悠久历史的程序设计方法。在几十年以来的大多数时间中,函数式程序设计栖身于象牙塔内,慢慢成长,积蓄力量,并没有在软件开发实践中得到广泛使用。然而,世异时移,这种情况正在发生变化。随着软件系统复杂性的不断提高以及软件与现实世界关系的日益紧密,如何严格确保软件系统的正确性和可靠性,逐渐成为一个重要的现实问题。函数式程序设计建立在严格的数学概念的基础上,具有良好的数学性质,为上述问题提供了一种现实可行的解决方案。目前,大多数的主流程序设计语言都在逐渐引入函数式程序设计的各种成分。
学习函数式程序设计的基本思想、核心概念、以及相关的程序设计方法和程序推理方法,能够为学生从事算法设计、程序语言设计、软件开发等领域的研究和实践工作建立坚实的理论基础。
—— 《计算概论A(实验班),函数式程序设计》课程介绍

我曾经是一个 OIer,会写很多的 C++,在初入北大的这个学期,我决定挑战一下自己的边界;于是我放弃了可以轻松刷绩点的普通计算概论A,选择了实验班,去接触陌生的函数式程序设计。

在期中考试之前,因为课程难度巨大、学习曲线陡峭,我因为担心期中考试挂科,有过退课的想法。

(“我真是个笨蛋”——函数式魔法少女并感)

但是我选择坚持挑战自己的边界,继续学了下去。

在期中考试中,我取得了 78.5 的成绩(满分 100),这是我这个菜菜的函数式魔法少女最高的期中考试成绩。虽然绝对分数低了点,但是按照北大的绩点计算方法,我已经上 3.0 了,这已经不错了。

现在,就让我这个最菜的函数式魔法少女,来给你们讲讲,为什么是函数式程序设计?

中毒

你们来这里上课的,大多都写过很多的 C++、Pascal、JAVA,我希望你们来这里上课时可以忘掉你们之前写过的那些东西,你们在 C++啊、JAVA啊里面中的毒太深了,这不利于你们学习函数式编程。
—— 《计算概论A(实验班),函数式程序设计》胡振江教授在第一节课上的讲话

在我看来,这是函数式程序设计的第一步,也是最重要的一步。

函数式程序设计和命令式程序设计虽然都叫“程序设计”,但是它们的思想是有着很大不同的,我们需要对各种各样的问题进行数学上的抽象,纯函数的抽象,而不是像命令式程序设计那样,用各种各样的语句来描述问题的解决过程。

所以说,忘掉那些你在命令式程序设计中学到的大部分东西,尤其是一些具有惯性的思维方式,达成思想上的“脱毒”,然后才能更好地学习函数式程序设计。

学函数式程序设计的好处

随着计算机硬件技术的逐渐进步,在一些一般的问题上,与主流的命令式语言相比,函数式程序设计语言带来的额外性能开销显得无足轻重;而保证软件系统的正确性和可靠性逐渐成为了软件工程中的主要矛盾。此时,函数式程序设计语言的优势就显现出来,由于数学上纯函数的性质和强大的静态强类型系统的存在,使得函数式语言写出来的程序自其诞生之初便是“纯净”、没有BUG的;这使得刚开始接触程序设计的初学者不会像学习命令式语言时那样,因为自己粗心大意而写出的BUG陷入调试的深渊无法自拔,而是可以写着纯净的函数式语言去解决一个又一个问题,为自己增加信心。
就学习的自然逻辑而言,函数式程序设计语言也有着独特的优势。我们都知道,要解决一个问题,首先应当做的是将它逐步分解成一个个简化的小问题,然后再针对这些变得平凡的简化问题提出解决方案。在写代码时,函数式语言基本上所需要做的是在数学上处理前一个步骤,分解与简化问题,而不太关心那些细枝末节的事情,颇有一种“大行不顾细谨,大礼不辞小让”的风格;而命令式语言则需要深入到硬件的层面细节(最典型的就是C语言的指针),去小心翼翼地处理各种琐碎的问题;让初学者过早地陷入细枝末节的斤斤计较中显然是不利于他们对于问题的整体把握和解决问题的素养的,而函数式语言通过编译器的“后台工作”巧妙地规避了这一点,也正因如此,函数式语言(以Haskell为例)有着自然语言般的可读性,和“一行顶十行”的简洁性,这对新人来讲无疑是友好的。

简洁性的一个例子,Haskell 中的归并排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
merge :: Ord a => [a] -> [a] -> [a]
merge x [] = x
merge [] y = y
merge (x:xs) (y:ys) = if x<=y then x : merge xs (y:ys) else y : merge (x:xs) ys

msort :: Ord a => [a] -> [a]
msort [] = []
msort [x] = [x]
msort seq = merge sbseq1 sbseq2
where
len=length seq
sbseq1=msort (take (len `div` 2) seq)
sbseq2=msort (drop (len `div` 2) seq)

为了不让学生们学到“无用之物”,我们还必须对函数式程序设计语言的应用前景做出回答。纯函数式编程语言虽然是小众,但是我们也应注意到,主流语言(如C++)在新世纪也普遍进行了一轮“函数式化”改造,将数学意义上的函数作为搭建程序大厦的“一等公民”的思想逐渐为人们所接受。在开发领域,Github、Facebook、Pandoc等知名网站和应用中都有大量使用Haskell开发的部分;近些年来Rust语言也是异军突起;而基于Haskell写成的Agda在程序证明和程序演算领域也发挥着它的作用。可以说,函数式程序设计语言在未来只会发挥出更大的力量。

可能遇到的问题

当然,本着客观严谨的态度,我们也不能只说函数式程序设计语言的好话,在函数式程序设计语言的推广中,依然存在着不少的问题。
首先,函数式编程语言通常有较高的学习曲线。它们的概念和语法可能与学习者以前使用的命令式编程语言不同,所以可能需要花费更多的时间来学习。这一点对于希望深入学习函数式编程语言的学习者尤为重要,命令式语言的学习无非是了解和掌握更多的特性,而函数式语言的深入则意味着在范畴论、类型论领域的艰深探索(虽然写Functor、Monad之类的结构并不需要开发者对这些知识的了解)。关于这个问题,则需要学校有优秀的师资力量执教函数式编程课程,同时不断迭代授课方式和内容,比如采用“翻转课堂”这样的形式来激发学生自主探索的兴趣,从而促进学生的学习效果。
其次,虽然函数式编程语言由于背后的自适应优化和惰性求值通常可以提供良好的性能,但是在某些情况下,它们可能不如命令式编程语言。例如,它们可能在处理大量数据或进行复杂计算时表现不佳。但是这一点不是很必要担心,因为处理不同问题本来就需要不同的方法,完全可以通过命令式语言处理这些需要高性能的问题,然后用基于函数式语言的程序证明工具来保证其正确性。
最后,虽然许多函数式编程语言都有强大的工具和库,但是它们可能没有像 Java 或 Python 这样的命令式编程语言那么丰富的生态系统。这也是和函数式编程语言长期栖居于学术界的象牙塔中,还未在工业界大展拳脚有关;这一点就只能留给我们这些未来有志于研究和应用函数式语言的开发者来解决了。

结论和启示

总而言之,函数式程序设计语言作为一颗当前在开发界升起的新星,还需要经受更多的时代的检验,在大学中推广和教学函数式语言也面临着阻碍和挑战。但做和不做是两件事情,不能因为学习函数式语言难、之前学习命令式语言“中毒太深”难以接受函数式语言、函数式语言的开发占比不高、学生退课多就轻易地放弃开课。尽管选择学习哪种语言是开发者或者学生自己的权利,但大学作为人类知识和学术的殿堂,应当为有志于此的后来者保留这种选择的权利。
虽然目前以卷积式神经网络为基底的生成式人工智能技术炙手可热,函数式语言在这方面并不适用,但学术的研究不能只是跟着潮流去走,许多重要的成果在它们展现出真正的力量之前总是被嘲笑为“无用之物”。函数式语言正是这样的一种“无用之物”,在工业界的不断“唱衰”中一步一步走着自己的路。也许在未来的某个地方,它终将绽放出美丽的花朵。

参考文献

[1]宋丽华,张兴元,王海涛.基于函数式编程的计算机专业基础理论教学改革实践[J].计算机教育,2021(01):133-136+141.DOI:10.16512/j.cnki.jsjjy.2021.01.032.
[2]乔海燕,周晓聪,杨永红.提高Haskell函数程序设计基础课程完成率的翻转课堂探讨[J].软件导刊,2022,21(05):229-232.
[3].结合面向对象和函数式语言的概念(英文)[J].软件学报,2000(01):8-22.DOI:10.13328/j.cnki.jos.2000.01.002.

后记

这是我的信概论文在博客上的移植,所以文风上可能会有一些参差,见谅!