【问题标题】:Why does a Scala for-comprehension have to start with a generator?为什么 Scala 的理解必须从生成器开始?
【发布时间】:2015-02-18 20:30:34
【问题描述】:

根据Scala Language Specification (§6.19),“枚举数序列总是以生成器开始”。为什么?

在将 for-comprehensions 与 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
}

确实,scalac 拒绝了此操作,并显示错误消息 '&lt;-' expected but '=' found

如果这是 Scala 中的有效语法,那么一个优点是Manager.getManager() 抛出的任何异常都将被for-comprehension 中使用的Future monad 捕获,并导致它产生一个失败的@ 987654329@,这是我想要的。在for-comprehension 之外调用Manager.getManager() 的解决方法没有这个优势:

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

  for {
    foo <- manager.makeFoo()
    value = foo.getValue()
  } yield value
}

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

我发现这个限制特别令人费解,因为在 Haskell 的其他类似 do 表示法中,没有要求 do 块应该以包含 &lt;- 的语句开头。谁能解释一下 Scala 和 Haskell 之间的区别?

这是一个完整的工作示例,展示了Future monad 如何在for-comprehensions 中捕获异常:

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

这是输出:

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

这是一个简单的示例,但我正在开展一个项目,其中我们有很多依赖于这种行为的非平凡代码。据我了解,这是使用Future(或Try)作为monad 的主要优势之一。我觉得奇怪的是我必须写

manager <- Future(Manager.getManager(crashGetManager))

而不是

manager = Manager.getManager(crashGetManager)

(编辑以反映 @RexKerr 的观点,即 monad 正在执行捕获异常的工作。)

【问题讨论】:

    标签: scala haskell monads


    【解决方案1】:

    for 理解不捕获异常Try可以,而且有相应的方法参与理解,所以你可以

    for {
      manager <- Try { Manager.getManager() }
      ...
    }
    

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

    所以我不确定你的前提是正确的。您在理解中所做的任何作业都可以尽早完成。

    (此外,在 for 理解中进行赋值只是为了产生那个确切的值是没有意义的。只需在 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
    

    即使您采用第一种类型,您仍然存在是否存在唯一“绑定”(包装方式)以及是否双重包装已经是正确类型的东西的问题。所有这些事情都可能有规则,但是理解已经很难学习了,不是吗?)

    【讨论】:

    • 在 for 理解中进行赋值可能有助于拆分太长/太复杂的 rhs 表达式并有助于提高可读性。就像在地图/平面图中引入局部变量一样。必须这样做可能是设计出现问题的迹象,但它仍然会有所帮助:)
    • 当与 Future 或 Try monad 一起使用时,异常确实会被理解。你自己试试,你会看到的。我同意我可以将对 Manager.getManager() 的调用包装在 Future 或 Try 构造函数中,我想这可能是最好的解决方法。但我不明白为什么我必须这样做。是的,正如@Jean 所指出的,为了理解通常需要进行大量计算,这些计算时间太长而无法放入 yield 块中。
    • @Jean - 您也可以在 yield 块中执行多行语句。不过,在某些情况下,拆分可能会更清晰。
    • @BenjaminGeer - 理解没有抓住任何东西。 monad 正在做所有的工作。因此,您希望编译器猜测您打算将初始分配包装在哪个 monad 中?如果它已经是正确的类型怎么办?无论如何都要重新包装?您可以为这些事情提出一致的规则,但我认为明确(明确)应该在这里获胜。此外:Manager.getManager() 到底在做什么返回非单子?这就是问题所在。如果您的方法没有“正确”输入,我不确定快速显式包装是否如此糟糕。
    • @BenjaminGeer - 这不是真的必要的,但对于比你更复杂的上下文来说,这是一个好主意。它可能以不同的方式实现:for 需要 M[A] &lt;: Monad[A] 并要求所有项目都是相同的类型,此时早期的定义将没有问题。但是做了一个不同的选择(更大的灵活性),所以在这一点上我认为它改变是不可取的,因为意外混淆或错误的危险很高。
    【解决方案2】:

    Haskell 将 for { manager = Manager.getManager(); ... } 的等价物转换为 lazy val manager = Manager.getManager(); for { ... } 的等价物。这似乎有效:

    scala> lazy val x: Int = throw new Exception("")
    x: Int = <lazy>
    
    scala> for { y <- Future(x + 1) } yield y
    res8: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise@fedb05d
    
    scala> Try(Await.result(res1, Duration("10 seconds")))
    res9: scala.util.Try[Int] = Failure(java.lang.Exception: )
    

    【讨论】:

      【解决方案3】:

      我认为无法做到这一点的原因是因为 for 循环是 flatMapmap 方法的语法糖(除非您在for 循环,在这种情况下,它会使用 withFilter 方法进行脱糖)。当您存储在不可变变量中时,您不能使用这些方法。这就是 Rex Kerr 指出的您可以使用 Try 的原因。在这种情况下,您应该能够使用 mapflatMap 方法。

      【讨论】:

      • 据我了解,Haskell 的 do 表示法也是类似函数调用的语法糖(Scala 的 flatMap 类似于 Haskell 的绑定)。那么为什么 Scala 的 for-comprehensions 不能提供与 Haskell 的 do 表示法相同的灵活性呢?
      猜你喜欢
      • 2014-04-19
      • 2015-05-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-12-02
      相关资源
      最近更新 更多