示例简单 IO 类型如何消除 "FP in Scala" 中的副作用?

How the sample Simple IO Type get rid of side effects in "FP in Scala"?

我正在阅读第 13.2.1 章并遇到了可以处理 IO 输入并同时消除副作用的示例:

object IO extends Monad[IO] {
  def unit[A](a: => A): IO[A] = new IO[A] { def run = a }
  def flatMap[A,B](fa: IO[A])(f: A => IO[B]) = fa flatMap f
  def apply[A](a: => A): IO[A] = unit(a)    
}

def ReadLine: IO[String] = IO { readLine }
def PrintLine(msg: String): IO[Unit] = IO { println(msg) }

def converter: IO[Unit] = for {
  _ <- PrintLine("Enter a temperature in degrees Fahrenheit: ")
  d <- ReadLine.map(_.toDouble)
  _ <- PrintLine(fahrenheitToCelsius(d).toString)
} yield ()

关于这段代码我有几个问题:

  1. unit函数中,def run = a到底做了什么?
  2. ReadLine函数中,IO { readLine }到底做了什么?它真的会执行 println 函数还是只是 return 一个 IO 类型?
  3. for理解中的_是什么意思(_ <- PrintLine("Enter a temperature in degrees Fahrenheit: "))?
  4. 为什么它消除了 IO 副作用?我看到这些功能仍然与输入和输出交互。

IO monad 视为一小段程序定义的最简单方法。

因此:

  1. 这是IO定义,run方法定义了IO monad的作用。 new IO[A] { def run = a } 是创建 class 实例和定义方法 run.
  2. 的 Scala 方法
  3. 有一点语法上的糖分。 IO { readLine }IO.apply { readLine }IO.apply(readLine) 相同,其中 readLine=> String 类型的按名称调用函数。这从 object IO 调用 unit 方法,因此这只是 IO class 的实例的创建,还没有 运行。
  4. 因为IO是一个monad,可以使用for comprehension。它需要以类似 result <- someMonad 的语法存储每个 monad 操作的结果。要忽略结果,可以使用 _,因此 _ <- someMonad 读取为执行 monad 但忽略结果。
  5. 这些方法都是 IO 定义,它们没有 运行 任何东西,因此没有副作用。只有在调用 IO.run 时才会出现副作用。
  1. 你的IO定义如下:

    trait IO { def run: Unit }
    

    按照这个定义,你可以理解写 new IO[A] { def run = a } 意味着从你的特征中初始化一个匿名的 class,并分配 a 成为你调用 [= 时运行的方法15=]。因为 aby name parameter,所以在创建时实际上没有 运行。

  2. Scala 中的任何对象或 class 遵循 apply 方法的约定,可以调用为:ClassName(args),编译器将在其中搜索object/class 上的 apply 方法并将其转换为 ClassName.apply(args) 调用。更详尽的答案 can be found here。因此,因为 IO 伴随对象拥有这样一个方法:

    def apply[A](a: => A): IO[A] = unit(a)    
    

    扩张是允许发生的。因此我们实际上调用了 IO.apply(readLine)

  3. _has many overloaded uses in Scala. This occurrence means "I don't care about the value returned from PrintLine, discard it". It is so because the value returned is of type Unit,与我们无关

  4. 并不是IO数据类型去掉了做IO的部分,而是延迟了[=56] =] 它到以后的时间点。我们通常说IO运行在应用程序的"edges"处,在Main方法中。这些与外部世界的交互仍然会发生,但是由于我们将它们封装在 IO 中,我们可以在我们的程序中将它们 作为值 进行推理,这带来了很多好处。例如,我们现在可以 组合 副作用并依赖于 success/failure 它们的执行。我们可以模拟这些 IO 效果 (using other data types such as Const),以及许多其他非常好的属性。