假设我有三个数据库访问函数foo
,bar
和baz
,每个函数都可以返回Option[A]
,其中A
是某个模型类,并且电话相互依赖。
我想按顺序调用函数,并且在每种情况下,如果找不到值,则返回相应的错误消息(None
)。
我目前的代码如下:
Input is a URL: /x/:xID/y/:yID/z/:zID
foo(xID) match {
case None => Left(s"$xID is not a valid id")
case Some(x) =>
bar(yID) match {
case None => Left(s"$yID is not a valid id")
case Some(y) =>
baz(zID) match {
case None => Left(s"$zID is not a valid id")
case Some(z) => Right(process(x, y, z))
}
}
}
可以看出,代码严重嵌套。
如果相反,我使用for
理解,我不能给出具体的错误信息,因为我不知道哪一步失败了:
(for {
x <- foo(xID)
y <- bar(yID)
z <- baz(zID)
} yield {
Right(process(x, y, z))
}).getOrElse(Left("One of the IDs was invalid, but we do not know which one"))
如果我使用map
和getOrElse
,我的代码几乎与第一个示例一样嵌套。
这是一种更好的方法来构建它以避免嵌套,同时允许特定的错误消息吗?
答案 0 :(得分:7)
您可以使用正确的投影来使for
循环正常工作。
def ckErr[A](id: String, f: String => Option[A]) = (f(id) match {
case None => Left(s"$id is not a valid id")
case Some(a) => Right(a)
}).right
for {
x <- ckErr(xID, foo)
y <- ckErr(yID, bar)
z <- ckErr(zID, baz)
} yield process(x,y,z)
这仍然有点笨拙,但它具有成为标准库的一部分的优势。
例外是另一种方法,但如果失败案例很常见,它们会减慢批次的速度。如果失败真是异常,我只会使用它。
也可以使用非本地回报,但这种特殊设置有点尴尬。我认为Either
的正确预测是可行的方法。如果你真的喜欢以这种方式工作但又不喜欢将.right
放在所有地方,那么有很多地方你可以找到一个“右偏的Either”,它默认就像正确的投影一样(例如ScalaUtils ,Scalaz等。)。
答案 1 :(得分:2)
我不会使用Option
而是使用Try
。这样你就可以将Monadic组合与保留错误的能力相结合。
def myDBAccess(..args..) =
thingThatDoesStuff(args) match{
case Some(x) => Success(x)
case None => Failure(new IdError(args))
}
我在上面假设你实际上并没有控制这些功能,也不能重构它们给你一个非Option
。如果您这样做,那么只需替换Try
。
答案 2 :(得分:1)
我知道这个问题在一段时间后得到了回答,但我想替代接受的答案。
鉴于此,在您的示例中,三个Option
是独立的,您可以将它们视为Applicative Functors,并使用Cats中的ValidatedNel
来简化和聚合处理不愉快的路径。
鉴于代码:
import cats.data.Validated.{invalidNel, valid}
def checkOption[B, T](t : Option[T])(ifNone : => B) : ValidatedNel[B, T] = t match {
case None => invalidNel(ifNone)
case Some(x) => valid(x)
def processUnwrappedData(a : Int, b : String, c : Boolean) : String = ???
val o1 : Option[Int] = ???
val o2 : Option[String] = ???
val o3 : Option[Boolean] = ???
然后你可以复制获得你想要的东西:
//import cats.syntax.cartesian._
(
checkOption(o1)(s"First option is not None") |@|
checkOption(o2)(s"Second option is not None") |@|
checkOption(o3)(s"Third option is not None")
) map (processUnwrappedData)
此方法将允许您聚合失败,这在您的解决方案中是不可能的(因为使用for-comprehensions强制执行顺序评估)。可以找到更多示例和文档here和{{3}}。
最后,此解决方案使用Cats Validated
,但很容易转换为Scalaz Validation
答案 3 :(得分:0)
我提出了这个解决方案(基于@Rex的解决方案及其评论):
def ifTrue[A](boolean: Boolean)(isFalse: => A): RightProjection[A, Unit.type] =
Either.cond(boolean, Unit, isFalse).right
def none[A](option: Option[_])(isSome: => A): RightProjection[A, Unit.type] =
Either.cond(option.isEmpty, Unit, isSome).right
def some[A, B](option: Option[A])(ifNone: => B): RightProjection[B, A] =
option.toRight(ifNone).right
他们执行以下操作:
ifTrue
时使用Boolean
,true
为“成功”情况(例如:isAllowed(userId)
)。它实际上会返回Unit
,因此应该在_ <- ifTrue(...) { error }
理解中用作for
。none
且Option
为“成功”的情况(例如:None
用于创建具有唯一电子邮件地址的帐户),则使用findUser(email)
。它实际上会返回Unit
,因此应该在_ <- none(...) { error }
理解中用作for
。some
且Option
为“成功”情况(例如:Some()
为findUser(userId)
),则使用GET /users/userId
。它返回Some
:user <- some(findUser(userId)) { s"user $userId not found" }
。它们用于for
理解:
for {
x <- some(foo(xID)) { s"$xID is not a valid id" }
y <- some(bar(yID)) { s"$yID is not a valid id" }
z <- some(baz(zID)) { s"$zID is not a valid id" }
} yield {
process(x, y, z)
}
这会返回Either[String, X]
,其中String
是错误消息,X
是调用process
的结果。