【问题标题】:Using for-comprehension, Try and sequences in Scala在 Scala 中使用 for-comprehension、Try 和序列
【发布时间】:2018-10-27 07:13:21
【问题描述】:

假设你有一堆方法:

def foo() : Try[Seq[String]]
def bar(s:String) : Try[String]

并且你想进行理解:

for {
  list <- foo
  item <- list
  result <- bar(item)
} yield result

当然这不会编译,因为在这种情况下 Seq 不能与 Try 一起使用。

任何人都有一个很好的解决方案,如何在不将其分成两个单独的 for 的情况下编写干净的代码?

我已经第三次遇到这个语法问题,并认为是时候问这个问题了。

【问题讨论】:

标签: scala for-comprehension


【解决方案1】:

恕我直言:TrySeq 不仅仅是定义 monad 转换器所需的:

库代码:

case class trySeq[R](run : Try[Seq[R]]) {
  def map[B](f : R => B): trySeq[B] = trySeq(run map { _ map f })
  def flatMap[B](f : R => trySeq[B]): trySeq[B] = trySeq {
    run match {
      case Success(s) => sequence(s map f map { _.run }).map { _.flatten }
      case Failure(e) => Failure(e)
    }
  }

  def sequence[R](seq : Seq[Try[R]]): Try[Seq[R]] = {
    seq match {
      case Success(h) :: tail =>
        tail.foldLeft(Try(h :: Nil)) {
          case (Success(acc), Success(elem)) => Success(elem :: acc)
          case (e : Failure[R], _) => e
          case (_, Failure(e)) => Failure(e)
        }
      case Failure(e) :: _  => Failure(e)
      case Nil => Try { Nil }
    }
  }
}

object trySeq {
  def withTry[R](run : Seq[R]): trySeq[R] = new trySeq(Try { run })
  def withSeq[R](run : Try[R]): trySeq[R] = new trySeq(run map (_ :: Nil))

  implicit def toTrySeqT[R](run : Try[Seq[R]]) = trySeq(run)
  implicit def fromTrySeqT[R](trySeqT : trySeq[R]) = trySeqT.run
} 

在您可以使用 for-comrehension 之后(只需导入您的库):

def foo : Try[Seq[String]] = Try { List("hello", "world") } 
def bar(s : String) : Try[String] = Try { s + "! " }

val x = for {
  item1  <- trySeq { foo }
  item2  <- trySeq { foo }
  result <- trySeq.withSeq { bar(item2) }
} yield item1 + result

println(x.run)

它适用于:

def foo() = Try { List("hello", throw new IllegalArgumentException()) } 
// x = Failure(java.lang.IllegalArgumentException)

【讨论】:

  • 我有一种直觉,这与某种单子魔法有关。我需要花一些时间学习这些东西。看起来真的很有益。
  • 完美答案。唯一要建议的 - 将“序列”方法移动到伴随对象
【解决方案2】:

您可以利用Try 可以转换为OptionOption 转换为Seq 的事实:

for {
  list <- foo.toOption.toSeq // toSeq needed here, as otherwise Option.flatMap will be used, rather than Seq.flatMap
  item <- list
  result <- bar(item).toOption // toSeq not needed here (but allowed), as it is implicitly converted
} yield result

这将返回一个(可能为空,如果Trys 失败)Seq

如果您想保留所有异常详细信息,则需要Try[Seq[Try[String]]]。这不能用单一的理解来完成,所以你最好坚持使用简单的map

foo map {_ map bar}

如果您想以不同的方式混合您的 Trys 和 Seqs,事情会变得更加复杂,因为没有自然的方法可以使 Try[Seq[Try[String]]] 变平。 @Yury 的回答展示了你必须做的事情。

或者,如果您只对代码的副作用感兴趣,您可以这样做:

for {
  list <- foo
  item <- list
  result <- bar(item)
} result

这是因为foreach 的类型签名限制较少。

【讨论】:

  • 问题是我会丢失异常细节。我想要实现的是处理 Seq 中的所有元素,如果有任何元素会抛出异常,则停止。除此之外,感谢 Option 的想法。一个不错的技巧。
  • 我更新了一些关于保留异常细节的选项的讨论。
【解决方案3】:

Try 可以转换为 Option,然后您可以将其用于理解。例如

scala> def testIt() = {
     |   val dividend = Try(Console.readLine("Enter an Int that you'd like to divide:\n").toInt)
     |   dividend.toOption
     | }
testIt: ()Option[Int]

scala> for (x <- testIt()) println (x * x)
Enter an Int that you'd like to divide:

scala> for (x <- testIt()) println (x * x)
Enter an Int that you'd like to divide:
1522756

我第一次输入“w”,然后第二次输入 1234。

【讨论】:

  • Try 也可以用于理解。但它不能与 SeqLike 的混合。存在类型匹配失败的问题,因为它需要 Try[...] 并且会找到 SeqLike。
  • "警告:DeprecatedConsole 类中的 readLine 方法已弃用:使用 scala.io.StdIn 中的方法"
猜你喜欢
  • 1970-01-01
  • 2011-04-19
  • 1970-01-01
  • 1970-01-01
  • 2014-07-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-03-24
相关资源
最近更新 更多