我最近观看了一段关于你如何能够提出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]]所以我们可以在发生异常时短路?请思考。
布莱尔
答案 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)。