Scala 3 — 书籍

纯函数

语言

Scala 为帮助你编写函数代码提供的另一项功能是编写纯函数的能力。纯函数可以这样定义

  • 如果给定相同的输入 x,函数 f 是纯函数,那么它始终返回相同的输出 f(x)
  • 该函数的输出取决于其输入变量及其实现
  • 它仅计算输出,而不修改其周围的世界

这意味着

  • 它不修改其输入参数
  • 它不会改变任何隐藏状态
  • 它没有任何“后门”:它不会从外部世界(包括控制台、网络服务、数据库、文件等)读取数据,也不会向外部世界写入数据

根据此定义,任何时候你使用相同输入值调用纯函数,你都将始终获得相同的结果。例如,你可以使用输入值 2 无限次调用 double 函数,你将始终获得结果 4

纯函数示例

根据该定义,你可以想象,scala.math._ 包中的以下方法是纯函数

  • abs
  • ceil
  • max

这些 String 方法也是纯函数

  • isEmpty
  • length
  • substring

Scala 集合类上的大多数方法也作为纯函数工作,包括 dropfiltermap 以及更多。

在 Scala 中,函数方法几乎完全可以互换,因此即使我们使用通用行业术语“纯函数”,此术语也可以用来描述函数和方法。如果您有兴趣了解如何将方法用作函数,请参阅 Eta 扩展 讨论。

不纯函数的示例

相反,以下函数是不纯的,因为它们违反了定义。

  • println – 与控制台、文件、数据库、Web 服务、传感器等交互的方法都是不纯的。
  • currentTimeMillis – 与日期和时间相关的函数都是不纯的,因为它们的输出取决于其输入参数之外的其他内容
  • sys.error – 抛出异常的方法是不纯的,因为它们不会简单地返回结果

不纯函数通常会执行以下一项或多项操作

  • 从隐藏状态读取,即它们访问未明确作为输入参数传递给函数的变量和数据
  • 写入隐藏状态
  • 改变它们给定的参数,或改变隐藏变量,例如它们包含的类中的字段
  • 与外部世界执行某种 I/O

通常,您应该注意返回类型为 Unit 的函数。由于这些函数不返回任何内容,因此从逻辑上讲,您调用它的唯一原因是实现某种副作用。因此,通常这些函数的使用是不纯的。

但需要不纯函数 …

当然,如果应用程序无法读写外部世界,那么它就没有什么用处,所以人们提出了这个建议

使用纯函数编写应用程序的核心,然后围绕该核心编写一个不纯的“包装器”以与外部世界进行交互。正如某人曾经说过的,这就像在纯蛋糕上放一层不纯的糖衣。

需要注意的是,有一些方法可以使与外部世界的交互不那么纯。例如,您会听说使用 IO 单子来处理输入和输出。这些主题超出了本文档的范围,因此为了简单起见,可以认为 FP 应用程序有一个纯函数的核心,这些函数用其他函数包装起来以与外部世界进行交互。

编写纯函数

注意:在本节中,常见的行业术语“纯函数”通常用于指代 Scala 方法。

要在 Scala 中编写纯函数,只需使用 Scala 的方法语法编写它们(虽然您也可以使用 Scala 的函数语法)。例如,这是一个将给定的输入值加倍的纯函数

def double(i: Int): Int = i * 2

如果您熟悉递归,这里有一个计算整数列表总和的纯函数

def sum(xs: List[Int]): Int = xs match {
  case Nil => 0
  case head :: tail => head + sum(tail)
}
def sum(xs: List[Int]): Int = xs match
  case Nil => 0
  case head :: tail => head + sum(tail)

如果您理解该代码,您会看到它符合纯函数定义。

要点

本节的第一个要点是纯函数的定义

纯函数 是一个只依赖于其声明的输入和实现来生成其输出的函数。它只计算其输出,并且不依赖或修改外部世界。

第二个要点是,每个现实世界的应用程序都与外部世界进行交互。因此,考虑函数式程序的一种简化方法是,它们由一个纯函数核心组成,该核心用与外部世界交互的其他函数包装起来。

本页的贡献者