为什么Scala for-comprehension必须从生成器开始?

Ben*_*eer 2 monads haskell scala

根据Scala语言规范(第6.19节),"枚举器序列始终以生成器开始".为什么?

我有时会发现这个限制在使用for-adrehension与monad 时是一个障碍,因为这意味着你不能做这样的事情:

def getFooValue(): Future[Int] = {
  for {
    manager = Manager.getManager() // could throw an exception
    foo <- manager.makeFoo() // method call returns a Future
    value = foo.getValue()
  } yield value
}
Run Code Online (Sandbox Code Playgroud)

实际上,scalac拒绝此错误消息'<-' expected but '=' found.

如果这是Scala中的有效语法,一个优点是抛出的任何异常Manager.getManager()都会被-comprehension中Future使用的monad for捕获,并且会导致它产生失败Future,这就是我想要的.将呼叫转移到Manager.getManager()外部for理解的解决方法没有这个优势:

def getFooValue(): Future[Int] = {
  val manager = Manager.getManager()

  for {
    foo <- manager.makeFoo()
    value = foo.getValue()
  } yield value
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,抛出的异常foo.getValue()将导致失败Future(这是我想要的),但抛出的异常Manager.getManager()将被抛回调用者getFooValue()(这不是我想要的).处理异常的其他可能方式更加冗长.

我发现这个限制特别令人费解,因为在Haskell的其他类似do符号中,没有要求do块应该以包含的语句开头<-.任何人都可以解释Scala和Haskell之间的这种差异吗?

这是一个完整的工作示例,显示Futuremonad in- forcomprehensions 如何捕获异常:

import scala.concurrent._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Try, Success, Failure}

class Foo(val value: Int) {
  def getValue(crash: Boolean): Int = {
    if (crash) {
      throw new Exception("failed to get value")
    } else {
      value
    }
  }
}

class Manager {
  def makeFoo(crash: Boolean): Future[Foo] = {
    if (crash) {
      throw new Exception("failed to make Foo")
    } else {
      Future(new Foo(10))
    }
  }
}

object Manager {
  def getManager(crash: Boolean): Manager = {
    if (crash) {
      throw new Exception("failed to get manager")
    } else {
      new Manager()
    }
  }
}

object Main extends App {

  def getFooValue(crashGetManager: Boolean,
                  crashMakeFoo: Boolean,
                  crashGetValue: Boolean): Future[Int] = {
    for {
      manager <- Future(Manager.getManager(crashGetManager))
      foo <- manager.makeFoo(crashMakeFoo)
      value = foo.getValue(crashGetValue)
    } yield value
  }

  def waitForValue(future: Future[Int]): Unit = {
    val result = Try(Await.result(future, Duration("10 seconds")))
    result match {
      case Success(value) => println(s"Got value: $value")
      case Failure(e) => println(s"Got error: $e")
    }
  }

  val future1 = getFooValue(false, false, false)
  waitForValue(future1)
  val future2 = getFooValue(true, false, false)
  waitForValue(future2)
  val future3 = getFooValue(false, true, false)
  waitForValue(future3)
  val future4 = getFooValue(false, false, true)
  waitForValue(future4)
}
Run Code Online (Sandbox Code Playgroud)

这是输出:

Got value: 10
Got error: java.lang.Exception: failed to get manager
Got error: java.lang.Exception: failed to make Foo
Got error: java.lang.Exception: failed to get value
Run Code Online (Sandbox Code Playgroud)

这是一个简单的例子,但我正在开发一个项目,其中我们有许多依赖于这种行为的非平凡代码.据我所知,这是使用Future(或Try)作为monad 的主要优点之一.我觉得奇怪的是我必须写

manager <- Future(Manager.getManager(crashGetManager))
Run Code Online (Sandbox Code Playgroud)

代替

manager = Manager.getManager(crashGetManager)
Run Code Online (Sandbox Code Playgroud)

(编辑反映@ RexKerr指出monad正在开展捕捉异常的工作.)

Rex*_*err 8

for理解不会抓住例外. Try是的,它有适当的方法来参与理解,所以你可以

for {
  manager <- Try { Manager.getManager() }
  ...
}
Run Code Online (Sandbox Code Playgroud)

但是Try除非你手动或隐式地有办法切换容器类型(例如转换Try为a的东西),否则它会一直向下List.

所以我不确定你的前提是否合适.您在for-comprehension中所做的任何任务都可以提前完成.

(另外,在for comprehension中进行赋值只是为了产生精确的值是没有意义的.只需在yield块中进行计算.)

(另外,只是为了说明多种类型可以在for理解中发挥作用,因此对于如何根据以后的类型包装早期作业,没有一个非常明显的正确答案:

// List and Option, via implicit conversion
for {i <- List(1,2,3); j <- Option(i).filter(_ <2)} yield j

// Custom compatible types with map/flatMap
// Use :paste in the REPL to define A and B together
class A[X] { def flatMap[Y](f: X => B[Y]): A[Y] = new A[Y] }
class B[X](x: X) { def map[Y](f: X => Y): B[Y] = new B(f(x)) }
for{ i <- (new A[Int]); j <- (new B(i)) } yield j.toString
Run Code Online (Sandbox Code Playgroud)

即使你采用第一种类型,你仍然有一个问题,即是否有一个独特的"绑定"(包装方式)以及是否双重包装已经是正确类型的东西.所有这些事情都可以有规则,但是理解力已经足够难以学习了,不是吗?)

  • @BenjaminGeer - for-comprehension没有抓到任何东西.monad正在做所有的工作.所以你希望编译器猜出你打算用哪个monad包装你的初始赋值?如果它已经是正确的类型怎么办?无论如何再次换行?你_could_为这些事情提出了一致的规则,但我认为明确(明确)应该在这里胜出.另外:什么是`Manager.getManager()`无论如何返回非monad?这就是问题的真正所在.如果您的方法没有"正确"输入,我不确定快速显式包装是如此糟糕. (4认同)