Scala IO monad:有什么意义?

时间:2013-10-30 15:44:51

标签: scala dependency-injection functional-programming monads

我最近观看了一段关于你如何能够提出IO monad的视频,谈话是在scala中进行的。我实际上想知道函数返回IO [A]是什么意思。包含在IO对象中的lambda表达式是突变的内容,并且在某些时候它们必须被观察到更高的变化,我的意思是执行,以便发生某些事情。你是不是只是将问题推到了其他地方的树上?

我能看到的唯一好处是它允许延迟评估,因为如果你不调用unsafePerformIO操作,就不会出现副作用。另外我猜这个程序的其他部分可以使用/共享代码并在它想要副作用发生时拒绝。

我想知道这是不是全部?可测试性有任何优势吗?我假设不是你必须观察那种否定这种效果的效果。如果使用了traits / interfaces,则可以控制依赖项,但不能在这些依赖项上发生效果时控制。

我在代码中汇总了以下示例。

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

}

你可以看到效果是如何推迟到主要我认为很酷。在从视频中感受到这一点后,我想出了这个。

我的实施是否正确,我的理解是否正确。

我也想知道是否应该将它与ValidationMonad结合使用,如ValidationMonad [IO [Person]]所以我们可以在发生异常时短路?请思考。

布莱尔

2 个答案:

答案 0 :(得分:28)

对于函数的类型签名来说,记录它是否有副作用很有价值。您的IO实现具有价值,因为它确实实现了这一点。它使您的代码更好地记录;如果您重构代码以尽可能地将逻辑从逻辑中分离出来而不是逻辑,那么您已经使非IO涉及的函数更易于组合和更易于测试。您可以在没有显式IO类型的情况下进行相同的重构;但使用显式类型意味着编译器可以帮助您进行分离。

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

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

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

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

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

更新(2015年12月):重新“交换IO行为的不同实现”,现在越来越多的人使用“免费monad”来做这类事情;见例如John De Goes的博客文章“A Modern Architecture for FP”。

答案 1 :(得分:19)

使用IO monad的好处是拥有纯粹的程序。你不会把副作用推到链条的上方,而是消除它们。如果你有如下不纯的功能:

def greet {
  println("What is your name?")
  val name = readLine
  println(s"Hello, $name!")
}

您可以通过将副作用重写为:

来消除副作用
def greet: IO[Unit] = for {
  _ <- putStrLn("What is your name?")
  name <- readLn
  _ <- putStrLn(s"Hello, $name!")
} yield ()

第二个功能是参考透明的。

可以在scala.io中找到使用IO monads导致纯程序的非常好的解释in Rúnar Bjarnason's slides(可以找到视频here)。