过程式程序中使用到的过程函数包括:
- Partial(偏函数) — 该函数不包含对某些输入的返回处理(例如,可能因为无法处理的输入导致异常)。
- Non-Deterministic(非确定性函数) — 该函数可能为相同的输入返回不同的输出。
- Impure(非纯函数) — 该函数或者会产生副作用,或使用可变量或访问外部值。
与过程式程序不同,函数式程序只使用纯函数,它包含以下特征:
- Total(全函数) — 函数总是为每一种可能的输入返回一个输出。
- Deterministic(确定性函数) — 函数总是为相同的输入返回相同的输出。
- Pure(纯函数) — 函数的唯一效果是完全根据输入来决定输出。
纯函数仅以完全确定的方式将输入值组合或转换为输出值。纯函数更易于理解,更易于测试,更易于重构和更易于抽象。
函数式程序不会直接与外部世界交互,因为这样将会带来偏向性,不确定性和副作用。相反,函数式程序会构建并返回数据结构
,并通过该数据结构描述(或建模)与现实世界的交互。
用于建模该过程 effect 的不可变数据结构称为函数式 effect
。这个概念对于深入了解 ZIO 的工作原理至关重要,将在下一节中介绍。
程序就是值
我们可以仅用三个指令来构建一个描述控制台程序的数据结构:
sealed trait Console[+A]
final case class Return[A](value: () => A) extends Console[A]
final case class PrintLine[A](line: String, rest: Console[A]) extends Console[A]
final case class ReadLine[A](rest: String => Console[A]) extends Console[A]
在此模型中,Console[A]
是一个不可变的,值类型安全的,返回 A
类型值的控制台程序。
下面这个 Console
数据结构是一个有序的“(语法)树”,在它“结尾”处,您会看到一条 Return
指令,该指令包含一个类型为 A
的值,该值是 Console[A]
程序的返回值。
尽管非常简单,但此数据结构足以构建交互式程序:
val example1: Console[Unit] =
PrintLine("Hello, what is your name?",
ReadLine(name =>
PrintLine(s"Good to meet you, ${name}", Return(() => ())))
)
这个不变量值没有做任何事情——它只是描述一个程序,该程序打印出一条消息,请求输入,然后打印出另一条取决于输入的消息。
尽管此程序只是一个模型,但我们可以使用解释器
将模型转换为过程效果,解释器将在数据结构上递归,将每条指令翻译为它描述的副作用:
def interpret[A](program: Console[A]): A = program match {
case Return(value) =>
value()
case PrintLine(line, next) =>
println(line)
interpret(next)
case ReadLine(next) =>
interpret(next(scala.io.StdIn.readLine()))
}
解释(也称为运行
或执行
)不是函数式的,因为它可能是偏函数式的,非确定性的或不纯的。在理想的应用程序中,解释只需发生一次:在应用程序的主函数中。而该应用程序的其余部分应该完全是纯函数式的。
实际上,直接使用构造函数构建控制台程序不是很方便。相反,我们可以定义辅助函数,这些辅助函数看起来更像它们的 effect 等效物:
def succeed[A](a: => A): Console[A] = Return(() => a)
def printLine(line: String): Console[Unit] =
PrintLine(line, succeed(()))
val readLine: Console[String] =
ReadLine(line => succeed(line))
如果我们在 Console
上定义 map
和 flatMap
方法,则将这些“叶子”指令组合成较大的程序将变得更加容易:
map
方法可通过提供函数A => B
将返回A
的 console 程序转换为返回B
的控制台程序。flatMap
方法使您可以按顺序将一个通过回调返回A
类型结果的控制台程序和另一个将A
作为输入的控制台程序组合在一起。
这两种函数的定义如下:
implicit class ConsoleSyntax[+A](self: Console[A]) {
def map[B](f: A => B): Console[B] =
flatMap(a => succeed(f(a)))
def flatMap[B](f: A => Console[B]): Console[B] =
self match {
case Return(value) => f(value())
case PrintLine(line, next) =>
PrintLine(line, next.flatMap(f))
case ReadLine(next) =>
ReadLine(line => next(line).flatMap(f))
}
}
借助这些 map
和 flatMap
方法,我们现在可以利用 Scala 的 for comprehensions
,编写看上去类似过程式的等效程序:
val example2: Console[String] =
for {
_ <- printLine("What's your name?")
name <- readLine
_ <- printLine(s"Hello, ${name}, good to meet you!")
} yield name
当我们希望执行该程序时,我们只需要在 Console
上调用解释器即可。
所有的函数式 Scala 程序都是这样构造的:与其直接与现实世界进行交互,不如建立一个函数式的 effect
,这无非是一个对过程进行建模的不可变的,类型安全的,类似树的数据结构。
函数式程序员使用函数 effect 来构建复杂的现实世界软件,同时,不放弃纯函数式编程所提供的等式推理,可组合性和类型安全性。
Next Steps
如果函数式 effect 对您来说开始变得更有意义,那么下一步就是进一步了解 ZIO 中的核心效果类型。