根据您询问的对象,函数式编程 (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。
即使在函数式语言中,您可以利用函数式编程的程度也存在很大差异。拥有类型系统会大有帮助,尤其是当它支持类型推断时(这样您不必总是键入类型)。本文没有篇幅详细介绍,但 suffice 只能说,并非所有类型系统都是相同的。
与所有语言一样,不同的函数式语言强调不同的概念、技术或用例。在选择语言时,重要的是要考虑它对函数式编程的支持程度以及是否适合您的用例。如果您被迫使用某种非 FP 语言,您仍然可以从应用该语言支持范围内的函数式编程中受益。
不要打开陷阱门!
回想一下,函数的结果仅取决于其输入。唉,几乎所有编程语言都具有破坏此假设的“特性”。空值、类型案例 (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)
- UPenn 的 CIS 194:Haskell 导论 是对函数式编程概念和实际 Haskell 开发的扎实介绍。课程资料是可用的,但讲座不可用(您可以观看布里斯班函数式编程小组 关于 CIS 194 的系列讲座 代替,那是几年前的)。
- 好的入门书籍包括 Scala 函数式编程、使用 Haskell 进行函数式思考 和 Haskell 编程从第一原理开始。
- Data61 FP 课程(前称为 NICTA 课程)通过类型驱动开发教授基础抽象和数据结构。回报是巨大的,但它在设计上是困难的,起源于培训研讨会,因此只有在您认识愿意指导您的函数式程序员时才尝试。
- 在您正在处理的任何代码中开始练习函数式编程。编写纯函数(避免不确定性和突变),使用高阶函数和递归代替循环,利用参数性来提高可读性和重用性。许多人通过在各种语言中进行实验和体验好处来开始函数式编程。
- 加入您所在地区的函数式编程用户组或学习小组——或创建一个——并关注函数式编程会议(新的会议不断涌现)。
结论
在本文中,我讨论了什么是函数式编程以及什么不是函数式编程,并研究了函数式编程的优势,包括等式推理和参数性。我们了解到,您可以在大多数编程语言中进行一些函数式编程,但语言的选择会影响您可以从中受益的程度,而 Haskell 等函数式编程语言 可以提供最多的好处。我还推荐了学习函数式编程的资源。
函数式编程是一个丰富的领域,还有许多更深入(更晦涩)的主题等待探索。我不能不提及一些具有实际意义的主题,例如
- lenses 和 prisms(一流的、可组合的 getter 和 setter;非常适合处理嵌套数据);
- 定理证明(当您可以证明代码正确时,为什么要测试代码呢?);
- 惰性求值(让您处理潜在的无限数据结构);
- 和范畴论(函数式编程中许多美观且实用的抽象的起源)。
我希望您喜欢这篇函数式编程导论,并受到启发,深入研究这种有趣且实用的软件开发方法。
本文根据 CC BY 4.0 许可发布。
9 条评论