函数式编程简介

我们解释什么是函数式编程,探讨其优点,并介绍学习函数式编程的资源。
518 位读者喜欢这篇文章。
How to use Python to hack your Eclipse IDE

Opensource.com

取决于你问谁,函数式编程 (FP) 要么是一种启蒙的编程方法,应该广泛传播,要么是一种过于学术化的编程方法,几乎没有实际的好处。在本文中,我将解释什么是函数式编程,探讨其优点,并推荐学习函数式编程的资源。

语法入门

本文中的代码示例使用 Haskell 编程语言。对于本文,您只需要理解基本函数语法

  even :: Int -> Bool
  even = ...    -- implementation goes here

这定义了一个名为 even 的单参数函数。第一行是类型声明,它表示 even 接受一个 Int 并返回一个 Bool。实现紧随其后,由一个或多个方程组成。我们将忽略实现(名称和类型足以说明问题)

  map :: (a -> b) -> [a] -> [b]
  map = ...

在此示例中,map 是一个接受两个参数的函数

  1. (a -> b):一个将 a 转换为 b 的函数
  2. [a]a 的列表

并返回一个 b 的列表。同样,我们不关心定义——类型更有趣!ab类型变量,可以代表任何类型。在下面的表达式中,aIntbBool

  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(匿名函数)和泛型的语言中有意义地进行函数式编程是困难的。大多数现代语言都具有这些功能,但在不同语言函数式编程的支持程度上存在差异。对函数式编程提供最佳支持的语言称为函数式编程语言。其中包括静态类型的 HaskellOCamlF#Scala,以及动态类型的 ErlangClojure

即使在函数式语言中,您可以在多大程度上利用函数式编程也存在很大差异。拥有类型系统会很有帮助,尤其是当它支持类型推断时(这样您就不必总是键入类型)。本文没有篇幅详细介绍,但足以说明,并非所有类型系统都是平等的。

与所有语言一样,不同的函数式语言强调不同的概念、技术或用例。在选择语言时,考虑它对函数式编程的支持程度以及它是否适合您的用例非常重要。如果您被迫使用某种非 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 是什么或如何构造一个。
  • 定理 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)可以提供最多的好处。我还推荐了学习函数式编程的资源。

函数式编程是一个丰富的领域,还有许多更深入(也更密集)的主题等待探索。我不能不提及一些具有实际意义的主题,例如

  • 镜头和棱镜(一流的、可组合的 getter 和 setter;非常适合处理嵌套数据);
  • 定理证明(当您可以证明其正确性时,为什么要测试您的代码?);
  • 惰性求值(让您处理潜在的无限数据结构);
  • 和范畴论(函数式编程中许多美丽而实用的抽象的起源)。

我希望您喜欢这篇函数式编程的介绍,并受到启发,深入研究这种有趣且实用的软件开发方法。

本文根据 CC BY 4.0 许可发布。

User profile image.
红帽软件工程师。对函数式编程、范畴论以及数学和编程的其他交叉领域感兴趣。对墨西哥辣椒着迷。

9 条评论

函数式编程中的函数是否需要产生两个输出:1) 正常输出,和 2) 下一个状态输出,因为如果没有生成状态,您就无法拥有复杂的计算机程序?

好问题 Tom。在这种情况下,输出的类型是“正常输出”和“下一个状态”的 2 元组或乘积。有一些很好的抽象可以用来构建程序以处理这种形式的状态,因此您不必总是显式地构造和解构(状态,输出)元组。

旁注:您可能会惊讶于您可以在没有状态的情况下完成什么!拥有状态可以更容易地表达程序或算法,但 Church-Turing 论题告诉我们,真正需要状态的算法……不存在 :)

回复 ,作者:Tom Borkowski (未验证)

Tom,是的,但大多数时候,“正常输出”和“下一个状态输出”是同一件事,例如斐波那契函数的结果。

回复 ,作者:Tom Borkowski (未验证)

感谢文章!
关于 Haskell 的入门书籍:Graham Hutton 的《Programming in Haskell (2nd Edition)》怎么样?您知道这本书是否适合 Haskell 初学者自学使用?

嗨 Florin,我以前不知道这本书,但我问了问。有些人读过它,并推荐给有决心的初学者。

回复 ,作者:florindan

我听说过很多关于《Learn you a Haskel for Great Good》的好评。我正在用 Javascript 学习 FP,并且已经订购了这本书来帮助我理解这些材料。

回复 ,作者:florindan

嗨 Matt。在我看来,LYAH 很适合学习 Haskell 语法,对于初学者程序员来说可能也不错。它很好地解释了很多事情,但在我看来,在教授 FP 的基础抽象方面做得不好(无论语言如何)。但这些只是我自己的经验,无论如何,这绝对是一本有趣且引人入胜的书。

回复 ,作者:Matt Perkins (未验证)

我看到的每篇“函数式编程入门”文章都解释了 FP 如何使用没有副作用的纯函数以及所有由此带来的巧妙之处。他们似乎都小心翼翼地避免谈及几乎所有有用的程序都需要副作用,以及如何将副作用与 FP 调和。毕竟,即使在屏幕上显示函数的结果也是一种副作用。

嗨 Norton。这有两个方面。

首先,许多语言允许您在任何地方进行 I/O,这引发了一个问题,即除了绝对必要的地方之外,是否值得避免它。这样做是值得的,因为它让您可以使用快速而宽松的推理。“函数式核心/命令式外壳”模式是如何在实践中实现这一点的示例。

其次,是否有一种在纯函数式程序中执行 I/O(或其他副作用)的方法(具有所有隐含的好处)?您可以 - 您只需要一个解释器来完成繁琐的部分。从这个意义上讲,纯程序就像一个菜谱:一组本身不做任何事情的指令。

回复 ,作者:Norton Allen (未验证)

© . All rights reserved.