取决于你问谁,函数式编程 (FP) 要么是一种启迪人心的编程方法,应该广泛传播,要么是一种过于学术化的编程方法,几乎没有实际应用的好处。在本文中,我将解释什么是函数式编程,探索其益处,并推荐学习函数式编程的资源。
语法入门
本文中的代码示例使用 Haskell 编程语言。您只需要理解本文的基本函数语法
even :: Int -> Bool
even = ... -- implementation goes here
这定义了一个名为 even 的单参数函数。第一行是类型声明,它表示 even 接受一个 Int 并返回一个 Bool。接下来是实现,它由一个或多个等式组成。我们将忽略实现(名称和类型足以告诉我们信息)
map :: (a -> b) -> [a] -> [b]
map = ...
在此示例中,map 是一个接受两个参数的函数
- (a -> b):一个将 a 转换为 b 的函数
- [a]:a 的列表
并返回 b 的列表。同样,我们不关心定义——类型更有趣!a 和 b 是类型变量,可以代表任何类型。在下面的表达式中,a 是 Int,b 是 Bool
map even [1,2,3]
它求值为 [Bool]
[False,True,False]
如果您看到其他不理解的语法,请不要惊慌;完全理解语法不是必要的。
关于函数式编程的误解
让我们首先消除常见的误解
- 函数式编程不是命令式编程或面向对象编程的竞争对手或对立面。这是一种错误的二分法。
- 函数式编程不仅仅是学术界的领域。诚然,函数式编程的历史深深植根于学术界,Haskell 和 OCaml 等语言是流行的研究语言。但如今,许多公司都在大型系统、小型专用程序以及介于两者之间的所有程序中使用函数式编程。甚至还有一个针对 函数式编程商业用户 的年度会议;过去的会议议程深入了解了函数式编程在行业中的应用方式以及应用者。
- 函数式编程与 monad 或任何其他特定抽象无关。尽管围绕这个话题争论不休,但 monad 只是一个具有法则的抽象。有些东西是 monad,有些则不是。
- 函数式编程并非特别难以学习。某些语言的语法或求值语义可能与您已经知道的不同,但这些差异是表面上的。函数式编程中存在深奥的概念,但这在其他方法中也是如此。
什么是函数式编程?
从本质上讲,函数式编程只是使用函数进行编程——纯数学函数。函数的结果仅取决于参数,并且没有副作用,例如 I/O 或状态的改变。程序是通过将函数组合在一起构建的。组合函数的一种方法是函数组合
(.) :: (b -> c) -> (a -> b) -> (a -> c)
(g . f) x = g (f x)
这个中缀函数将两个函数组合成一个,将 g 应用于 f 的输出。我们将在接下来的示例中看到它的用法。为了比较,Python 中相同的函数看起来像
def compose(g, f):
return lambda x: g(f(x))
函数式编程的优点在于,由于函数是确定性的并且没有副作用,因此您可以始终用函数应用的结果替换函数应用。这种等量代换启用了等式推理。每个程序员都必须推理自己和他人的代码,而等式推理是执行此操作的绝佳工具。让我们看一个例子。您遇到表达式
map even . map (+1)
这个程序做什么?可以简化吗?等式推理让您可以通过一系列替换来分析代码
map even . map (+1)
map (even . (+1)) -- from definition of 'map'
map (\x -> even (x + 1)) -- lambda abstraction
map odd -- from definition of 'even'
我们可以使用等式推理来理解程序并优化可读性。Haskell 编译器使用等式推理来执行多种程序优化。如果没有纯函数,等式推理要么不可能,要么需要程序员付出过多的努力。
函数式编程语言
您需要从编程语言中获得什么才能进行函数式编程?
在没有高阶函数(将函数作为参数传递和返回函数的能力)、lambda 表达式(匿名函数)和泛型的语言中,有意义地进行函数式编程是困难的。大多数现代语言都具有这些功能,但是不同语言对函数式编程的支持程度存在差异。对函数式编程支持最好的语言称为函数式编程语言。其中包括静态类型的 Haskell、OCaml、F# 和 Scala,以及动态类型的 Erlang 和 Clojure。
即使在函数式语言中,您可以在多大程度上利用函数式编程也存在很大差异。拥有类型系统会很有帮助,尤其是当它支持类型推断时(这样您就不必总是键入类型)。本文没有空间详细介绍,但可以说,并非所有类型系统都是相同的。
与所有语言一样,不同的函数式语言强调不同的概念、技术或用例。在选择语言时,考虑它对函数式编程的支持程度以及它是否适合您的用例非常重要。如果您被迫使用某些非函数式编程语言,您仍然可以从应用语言所支持程度的函数式编程中获益。
不要打开那个陷阱门!
回想一下,函数的结果仅取决于其输入。唉,几乎所有的编程语言都有“特性”打破了这个假设。空值、类型判断 (instanceof)、类型转换、异常、副作用和无限递归的可能性都是破坏等式推理并削弱程序员推理程序行为或正确性的能力的陷阱。(完全语言,它们没有任何陷阱门,包括 Agda、Idris 和 Coq。)
幸运的是,作为程序员,我们可以选择避免这些陷阱,如果我们有纪律,我们可以假装陷阱门不存在。这个想法被称为快速而宽松的推理。它几乎不花费任何代价——几乎任何程序都可以在不使用陷阱门的情况下编写——通过避免它们,您重新获得了等式推理、可组合性和重用。
让我们详细讨论异常。这个陷阱门破坏了等式推理,因为异常终止的可能性没有在类型中反映出来。(如果文档甚至提到可能抛出的异常,您就应该感到幸运。)但是,我们没有理由不能拥有一个包含所有失败模式的返回类型。
避免陷阱门是语言特性可以发挥重要作用的领域。为了避免异常,可以使用代数数据类型来模拟错误条件,如下所示
-- new data type for results of computations that can fail
--
data Result e a = Error e | Success a
-- new data type for three kinds of arithmetic errors
--
data ArithError = DivByZero | Overflow | Underflow
-- integer division, accounting for divide-by-zero
--
safeDiv :: Int -> Int -> Result ArithError Int
safeDiv x y =
if y == 0
then Error DivByZero
else Success (div x y)
此示例中的权衡是,您现在必须使用 Result ArithError Int 类型的值而不是普通的 Int,但是有一些抽象可以处理这个问题。您不再需要处理异常,并且可以使用快速而宽松的推理,因此总的来说这是一个胜利。
免费定理
大多数现代静态类型语言都具有泛型(也称为参数多态),其中函数是在一个或多个抽象类型上定义的。例如,考虑一个关于列表的函数
f :: [a] -> [a]
f = ...
Java 中相同的函数看起来像
static <A> List<A> f(List<A> xs) { ... }
编译后的程序证明了此函数将适用于类型 a 的任何选择。考虑到这一点,并采用快速而宽松的推理,您能弄清楚该函数的作用吗?了解类型有帮助吗?
在这种情况下,类型并没有完全告诉我们该函数的作用(它可以反转列表、删除第一个元素或许多其他事情),但它确实告诉了我们很多。仅从类型来看,我们就可以推导出关于该函数的定理
- 定理 1:输出中的每个元素都出现在输入中;它不可能向列表中添加 a,因为它不知道 a 是什么或如何构造一个 a。
- 定理 2:如果您将任何函数映射到列表上,然后应用 f,则结果与先应用 f 然后映射相同。
定理 1 帮助我们理解代码正在做什么,定理 2 对于程序优化很有用。我们仅从类型中就了解了所有这些!这种结果——从类型推导出有用的定理的能力——称为参数性。由此可见,类型是函数行为的部分(有时是完整的)规范,以及一种机器检查的文档。
现在轮到您利用参数性了。您可以从 map 和 (.) 的类型或以下函数中得出什么结论?
- foo :: a -> (a, a)
- bar :: a -> a -> a
- baz :: b -> a -> a
学习函数式编程的资源
也许您已经确信函数式编程是编写软件的更好方法,并且您想知道如何开始入门?有几种学习函数式编程的方法;以下是我推荐的一些方法(我承认,我强烈偏向 Haskell)
- 宾夕法尼亚大学的 CIS 194:Haskell 导论 是对函数式编程概念和真实 Haskell 开发的扎实介绍。课程材料是可用的,但讲座不可用(您可以观看布里斯班函数式编程小组的 关于 CIS 194 的系列讲座,这是几年前的讲座)。
- 好的入门书籍包括 Scala 函数式编程、使用 Haskell 进行函数式思考 和 Haskell 编程从第一性原理开始。
- Data61 FP 课程 (f.k.a., NICTA 课程) 通过类型驱动开发教授基础抽象和数据结构。回报是巨大的,但设计上是困难的,它起源于培训研讨会,因此只有在您认识一位愿意指导您的函数式程序员时才尝试它。
- 开始在您正在处理的任何代码中练习函数式编程。编写纯函数(避免非确定性和改变),使用高阶函数和递归而不是循环,利用参数性来提高可读性和重用性。许多人通过在各种语言中进行实验和体验好处来开始函数式编程。
- 加入您所在地区的函数式编程用户组或学习小组——或自己创建一个——并留意函数式编程会议(新的会议不断涌现)。
结论
在本文中,我讨论了函数式编程是什么和不是什么,并研究了函数式编程的优势,包括等式推理和参数性。我们了解到您可以在大多数编程语言中进行一些函数式编程,但是语言的选择会影响您可以从中受益多少,而 Haskell 等函数式编程语言可以提供最多的益处。我还推荐了学习函数式编程的资源。
函数式编程是一个丰富的领域,还有许多更深入(更深奥)的主题等待探索。我不能不提及一些具有实际意义的主题,例如
- 透镜和棱镜(一等公民、可组合的 getter 和 setter;非常适合处理嵌套数据);
- 定理证明(当您可以证明其正确性时,为什么要测试您的代码?);
- 惰性求值(让您处理潜在的无限数据结构);
- 以及范畴论(函数式编程中许多美丽而实用的抽象的起源)。
我希望您喜欢这篇函数式编程导论,并受到启发去深入研究这种有趣且实用的软件开发方法。
本文根据 CC BY 4.0 许可协议发布。
9 条评论