根据您询问的对象,函数式编程 (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 等语言是流行的研究语言。但如今,许多公司都在大型系统、小型专用程序以及介于两者之间的所有程序中使用函数式编程。甚至还有一个关于 函数式编程商业用户 的年度会议;过去的会议议程深入了解了函数式编程在工业界的使用方式以及使用者。
- 函数式编程与 monads 或任何其他特定抽象无关。尽管围绕这个话题争论不休,但 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 编译器使用等式推理来执行多种程序优化。如果没有纯函数,等式推理要么不可能,要么需要程序员付出过度的努力。
函数式编程语言
为了能够进行函数式编程,您需要编程语言提供什么?
在没有高阶函数(将函数作为参数传递并返回函数的能力)、lambdas(匿名函数)和泛型的语言中,有意义地进行函数式编程是很困难的。大多数现代语言都具备这些功能,但不同语言对函数式编程的支持程度存在差异。支持最好的语言称为函数式编程语言。这些语言包括静态类型的 Haskell、OCaml、F# 和 Scala,以及动态类型的 Erlang 和 Clojure。
即使在函数式语言中,您在多大程度上可以利用函数式编程也存在很大差异。拥有类型系统有很大帮助,特别是如果它支持类型推断(这样您就不必总是键入类型)。本文没有篇幅详细介绍,但足以说明,并非所有类型系统都是相同的。
与所有语言一样,不同的函数式语言强调不同的概念、技术或用例。在选择语言时,考虑它对函数式编程的支持程度以及它是否适合您的用例非常重要。如果您被迫使用某些非 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 有很强的偏见)
- 宾夕法尼亚大学的 CIS 194:Haskell 入门 是对函数式编程概念和真实世界 Haskell 开发的可靠介绍。课程资料是可用的,但讲座不可用(您可以观看布里斯班函数式编程小组的 涵盖 CIS 194 的系列讲座,这些讲座是几年前的)。
- 好的入门书籍包括 Scala 函数式编程、使用 Haskell 进行函数式思考 和 Haskell 编程从第一原理开始。
- Data61 FP 课程(前身为 NICTA 课程)通过类型驱动开发教授基础抽象和数据结构。回报是巨大的,但它在设计上很困难,起源于培训研讨会,因此只有在您认识一位愿意指导您的函数式程序员时才尝试它。
- 开始在您正在处理的任何代码中练习函数式编程。编写纯函数(避免非确定性和突变),使用高阶函数和递归而不是循环,利用参数性来提高可读性和重用性。许多人通过在各种语言中进行实验和体验好处来开始函数式编程。
- 加入您所在地区的函数式编程用户组或学习小组——或创建一个——并留意函数式编程会议(新的会议不断涌现)。
结论
在本文中,我讨论了函数式编程是什么和不是什么,并研究了函数式编程的优势,包括等式推理和参数性。我们了解到,您可以在大多数编程语言中进行一些函数式编程,但语言的选择会影响您可以从中受益多少,函数式编程语言(例如 Haskell)提供的最多。我还推荐了学习函数式编程的资源。
函数式编程是一个丰富的领域,还有许多更深入(也更密集)的主题等待探索。我不应该忘记提及一些具有实际意义的主题,例如
- lenses 和 prisms(一流的、可组合的 getters 和 setters;非常适合处理嵌套数据);
- 定理证明(当您可以证明代码正确时,为什么要测试您的代码呢?);
- 惰性求值(让您处理潜在的无限数据结构);
- 以及范畴论(函数式编程中许多美观且实用的抽象的起源)。
我希望您喜欢这篇函数式编程入门,并受到启发,投入到这种有趣而实用的软件开发方法中。
本文根据 CC BY 4.0 许可发布。
9 条评论