背景知识

过程式程序中使用到的过程函数包括:

  • 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 上定义 mapflatMap 方法,则将这些“叶子”指令组合成较大的程序将变得更加容易:

  • 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))
    }
}

借助这些 mapflatMap 方法,我们现在可以利用 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 中的核心效果类型

Leave a Reply
Your email address will not be published.
*
*

BACK TO TOP