Scala IO 单子(monad) : what's the point?

标签 scala dependency-injection functional-programming monads

我最近观看了一个关于如何提出 IO monad 的视频,演讲是用 scala 进行的。我实际上想知道让函数返回 IO[A] 的意义何在。包装在 IO 对象中的 lambda 表达式就是突变,在更高的某个时刻,它们必须被观察到,我的意思是执行,以便发生一些事情。您不只是将问题推到更高的位置吗?

我能看到的唯一好处是它允许延迟计算,从某种意义上说,如果您不调用 unsafePerformIO 操作,则不会发生副作用。另外我猜想程序的其他部分可以使用/共享代码并决定何时发生副作用。

我想知道这就是全部吗?在可测试性方面有什么优势吗?我假设不是,因为你必须观察抵消这一点的影响。如果您使用特征/接口(interface),您可以控制依赖关系,但当这些依赖关系发生影响时则无法控制。

我将以下示例放在代码中。

case class IO[+A](val ra: () => A){
  def unsafePerformIO() : A = ra();
  def map[B](f: A => B) : IO[B] = IO[B]( () => f(unsafePerformIO()))
  def flatMap[B](f: A => IO[B]) : IO[B] = {
    IO( () =>  f(ra()).unsafePerformIO())
  }
}



case class Person(age: Int, name: String)

object Runner {

  def getOlderPerson(p1: Person,p2:Person) : Person = 
    if(p1.age > p2.age) 
        p1
      else
        p2

  def printOlder(p1: Person, p2: Person): IO[Unit] = {
    IO( () => println(getOlderPerson(p1,p2)) ).map( x => println("Next") )
  }

  def printPerson(p:Person) = IO(() => {
    println(p)
    p
  })

  def main(args: Array[String]): Unit = {

    val result = printPerson(Person(31,"Blair")).flatMap(a => printPerson(Person(23,"Tom"))
                                   .flatMap(b => printOlder(a,b)))

   result.unsafePerformIO()
  }

}

你可以看到效果是如何推迟到 main 的,我认为这很酷。我是从视频中感受到这一点后想到的。

我的实现是否正确以及我的理解是否正确。

我还想知道是否应该将里程与 ValidationMonad 结合起来,如 ValidationMonad[IO[Person]] 中那样,以便在发生异常时可以短路?请思考。

布莱尔

最佳答案

函数的类型签名对于记录它是否有副作用是很有值(value)的。您的 IO 实现很有值(value),因为它确实完成了这么多工作。它使您的代码得到更好的记录;如果您重构代码以尽可能地将涉及 IO 的逻辑与不涉及 IO 的逻辑分开,那么您就使不涉及 IO 的函数变得更可组合且更可测试。您可以在没有显式 IO 类型的情况下进行相同的重构;但使用显式类型意味着编译器可以帮助您进行分离。

但这只是开始。在您问题的代码中,IO 操作被编码为 lambda,因此是不透明的;除了运行 IO 操作之外,您无法对它执行任何操作,并且运行时的效果是硬编码的。

这不是实现 IO monad 的唯一可能方法。

例如,我可能会创建扩展共同特征的 IO 操作案例类。例如,然后我可以编写一个测试来运行一个函数并查看它是否返回正确的 IO 操作种类

在这些代表不同类型 IO 操作的类的情况下,我可能不会包含这些操作在运行时执行的操作的硬编码实现。相反,我可以使用类型类模式将其解耦。这将允许交换 IO 操作的不同实现。例如,我可能有一组与生产数据库通信的实现,以及另一组与模拟内存数据库通信以用于测试目的的实现。

Bjarnason 和 Chiusano 的《Scala 函数式编程》一书的第 13 章(“外部效果和 I/O”)对这些问题进行了很好的处理。特别参见第 13.2.2 节“简单 IO 类型的优点和缺点”。

更新:关于“交换 IO 操作的不同实现”,您可能会查找“free monad”,这是一种安排方法。同样相关的是“无标记最终”风格,在这种风格中,您可以独立于具体类型(例如 IO)编写单子(monad)代码。

关于Scala IO 单子(monad) : what's the point?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/19687470/

相关文章:

python - 从函数式语言访问 numpy 数组

virtual - 细粒度沙箱

java - 在 JSoup 文档中获取所有节点(递归)的最快方法

dependency-injection - 如何从域对象内分派(dispatch)域事件?

functional-programming - 如何判断变量的值是否是绑定(bind)到 Scheme 中的过程的符号?

wpf - WPF 中的应用程序变量,同时保持可测试性

Scala 蛋糕模式鼓励硬编码依赖关系?

java - 玩法 2 - 没有助手的模板

scala - 格式化一个简单的字符串,但 `java.lang.NoSuchMethodError`

scala - 将 paytm 回调 url 设置为本地主机