在 Scala 中避免深度嵌套的选项级联
Avoiding deeply nested Option cascades in Scala
假设我有三个数据库访问函数 foo
、bar
和 baz
,每个 return Option[A]
其中 A
是某些模型 class,调用相互依赖。
我想按顺序调用这些函数,在每种情况下,如果找不到该值,return 会显示一条适当的错误消息 (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
,我最终得到的代码几乎与第一个示例一样嵌套。
是否有更好的结构方式来避免嵌套,同时允许特定的错误消息?
我不会使用 Option
,而是使用 Try
。这样你就拥有了你想要的 Monadic 组合和保留错误的能力。
def myDBAccess(..args..) =
thingThatDoesStuff(args) match{
case Some(x) => Success(x)
case None => Failure(new IdError(args))
}
我在上面假设你实际上并没有控制这些功能,也不能重构它们给你一个非Option
。如果你这样做了,那么只需替换 Try
.
您可以通过使用正确的投影来使 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)
这仍然有点笨拙,但它的优点是作为标准库的一部分。
异常是另一种方法,但如果失败案例很常见,它们会使速度减慢很多。如果失败真的很特别,我只会使用它。
也可以使用非本地 returns,但对于这个特定的设置来说有点尴尬。我认为 Either
的正确预测是可行的方法。如果你真的喜欢这样工作,但不喜欢把 .right
到处都是,你可以在很多地方找到 "right-biased Either",默认情况下它就像正确的投影(例如 ScalaUtils, Scalaz 等)。
我想到了这个解决方案(基于@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
用于函数 returns a Boolean
,true
是 "success" 的情况(例如:isAllowed(userId)
) .它实际上 returns Unit
所以应该在 for
理解中用作 _ <- ifTrue(...) { error }
。
none
用于函数 returns 和 Option
且 None
是 "success" 的情况(例如:findUser(email)
用于创建具有唯一电子邮件地址的帐户)。它实际上 returns Unit
所以应该在 for
理解中用作 _ <- none(...) { error }
。
some
用于函数 returns 和 Option
且 Some()
是 "success" 的情况(例如:findUser(userId)
用于GET /users/userId
)。它returns的内容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)
}
This returns an Either[String, X]
其中 String
是一条错误消息,X
是调用 process
.[=38= 的结果]
我知道这个问题很久以前就有人回答了,但我想提供一个已接受答案的替代方案。
鉴于在您的示例中,三个 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 and here.
最后这个解决方案使用了 Cats Validated
但可以很容易地转换为 Scalaz Validation
假设我有三个数据库访问函数 foo
、bar
和 baz
,每个 return Option[A]
其中 A
是某些模型 class,调用相互依赖。
我想按顺序调用这些函数,在每种情况下,如果找不到该值,return 会显示一条适当的错误消息 (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
,我最终得到的代码几乎与第一个示例一样嵌套。
是否有更好的结构方式来避免嵌套,同时允许特定的错误消息?
我不会使用 Option
,而是使用 Try
。这样你就拥有了你想要的 Monadic 组合和保留错误的能力。
def myDBAccess(..args..) =
thingThatDoesStuff(args) match{
case Some(x) => Success(x)
case None => Failure(new IdError(args))
}
我在上面假设你实际上并没有控制这些功能,也不能重构它们给你一个非Option
。如果你这样做了,那么只需替换 Try
.
您可以通过使用正确的投影来使 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)
这仍然有点笨拙,但它的优点是作为标准库的一部分。
异常是另一种方法,但如果失败案例很常见,它们会使速度减慢很多。如果失败真的很特别,我只会使用它。
也可以使用非本地 returns,但对于这个特定的设置来说有点尴尬。我认为 Either
的正确预测是可行的方法。如果你真的喜欢这样工作,但不喜欢把 .right
到处都是,你可以在很多地方找到 "right-biased Either",默认情况下它就像正确的投影(例如 ScalaUtils, Scalaz 等)。
我想到了这个解决方案(基于@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
用于函数 returns aBoolean
,true
是 "success" 的情况(例如:isAllowed(userId)
) .它实际上 returnsUnit
所以应该在for
理解中用作_ <- ifTrue(...) { error }
。none
用于函数 returns 和Option
且None
是 "success" 的情况(例如:findUser(email)
用于创建具有唯一电子邮件地址的帐户)。它实际上 returnsUnit
所以应该在for
理解中用作_ <- none(...) { error }
。some
用于函数 returns 和Option
且Some()
是 "success" 的情况(例如:findUser(userId)
用于GET /users/userId
)。它returns的内容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)
}
This returns an Either[String, X]
其中 String
是一条错误消息,X
是调用 process
.[=38= 的结果]
我知道这个问题很久以前就有人回答了,但我想提供一个已接受答案的替代方案。
鉴于在您的示例中,三个 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 and here.
最后这个解决方案使用了 Cats Validated
但可以很容易地转换为 Scalaz Validation