【问题标题】:scala: how to handle validations in a functional wayscala:如何以功能方式处理验证
【发布时间】:2012-08-22 06:17:41
【问题描述】:

我正在开发一种方法,如果它通过条件列表,它应该可以持久化一个对象。

如果任何(或许多)条件失败(或出现任何其他类型的错误),则应返回包含错误的列表,如果一切顺利,则应返回已保存的实体。

我在想这样的事情(当然是伪代码):

request.body.asJson.map { json =>
  json.asOpt[Wine].map { wine =>
    wine.save.map { wine => 
      Ok(toJson(wine.update).toString)
    }.getOrElse  { errors => BadRequest(toJson(errors))}
  }.getOrElse    { BadRequest(toJson(Error("Invalid Wine entity")))}
}.getOrElse      { BadRequest(toJson(Error("Expecting JSON data")))}

也就是说,我想把它当作一个 Option[T] 来对待,如果任何验证失败,而不是返回 None 它会给我错误列表...

这个想法是返回一个 JSON 错误数组...

所以问题是,这是处理这种情况的正确方法吗?在 Scala 中实现它的方法是什么?

--

糟糕,刚刚发布了问题并发现了任何一个

http://www.scala-lang.org/api/current/scala/Either.html

无论如何,我想知道您对所选择的方法的看法,以及是否有其他更好的选择来处理它。

【问题讨论】:

标签: scala error-handling


【解决方案1】:

使用 scalaz 你有Validation[E, A],它类似于Either[E, A],但是如果E 是一个半群(意思是可以连接的东西,比如列表),那么多个经过验证的结果可以以某种方式组合保留所有发生的错误。

以 Scala 2.10-M6 和 Scalaz 7.0.0-M2 为例,其中 Scalaz 有一个名为 \/[L, R] 的自定义 Either[L, R],默认为右偏:

import scalaz._, Scalaz._

implicit class EitherPimp[E, A](val e: E \/ A) extends AnyVal {
  def vnel: ValidationNEL[E, A] = e.validation.toValidationNEL
}

def parseInt(userInput: String): Throwable \/ Int = ???
def fetchTemperature: Throwable \/ Int = ???
def fetchTweets(count: Int): Throwable \/ List[String] = ???

val res = (fetchTemperature.vnel |@| fetchTweets(5).vnel) { case (temp, tweets) =>
  s"In $temp degrees people tweet ${tweets.size}"
}

这里的result 是一个Validation[NonEmptyList[Throwable], String],包含所有发生的错误(温度传感器错误和/或twitter 错误或无)或成功消息。然后您可以切换回\/ 以方便使用。

注意:Either 和 Validation 的区别主要在于 Validation 可以累积错误,但不能 flatMap 丢失累积的错误,而使用 Either 不能(轻松)累积但可以 flatMap(或为了便于理解)并且可能会丢失除第一条错误消息之外的所有内容。

关于错误层次结构

我认为这可能会让您感兴趣。无论使用 scalaz/Either/\//Validation,我都体验到入门很容易,但继续前进需要一些额外的工作。问题是,如何以有意义的方式从多个错误函数中收集错误?当然,您可以在任何地方使用 ThrowableList[String] 并享受轻松的时光,但听起来不太实用或可解释。想象一下得到一个错误列表,如“缺少儿童年龄”::“IO 错误读取文件”::“除以零”。

所以我的选择是创建错误层次结构(使用 ADT-s),就像将 Java 的已检查异常包装到层次结构中一样。例如:

object errors {

  object gamestart {
    sealed trait Error
    case class ResourceError(e: errors.resource.Error) extends Error
    case class WordSourceError(e: errors.wordsource.Error) extends Error
  }

  object resource {
    case class Error(e: GdxRuntimeException)
  }

  object wordsource {
    case class Error(e: /*Ugly*/ Any)
  }

}

然后,当使用具有不同错误类型的错误函数的结果时,我将它们加入到相关的父错误类型下。

for {
  wordSource <-
    errors.gamestart.WordSourceError <-:
    errors.wordsource.Error <-:
    wordSourceCreator.doCreateWordSource(mtRandom).catchLeft.unsafePerformIO.toEither

  resources <-
    errors.gamestart.ResourceError <-:
    GameViewResources(layout)

} yield ...

这里f &lt;-: e 映射了e: \/ 左侧的函数f,因为\/ 是一个双函子。对于se: scala.Either,您可能有se.left.map(f)

这可以通过提供shapelessHListIsos 来进一步改进,以便能够绘制漂亮的错误树。

修订

更新:(e: \/).vnel 将失败端提升为NonEmptyList,因此如果我们遇到失败,我们至少有一个错误(是:或没有)。

【讨论】:

  • 我认为Validation 不适合 OP 的问题,因为在示例中,一个计算(将 JSON 转换为 Wine)取决于先前计算的结果(从请求中提取 JSON) .所以它们必须被排序并且会快速失败,而不是累积错误。
  • James:“如果任何(或许多)条件失败(或出现任何其他类型的错误),则应返回包含错误的列表” - 听起来像是 Validation 的地方。注意EitherValidation可以按需混用,积累后可以回Either[List[Throwable], A]
  • +1 表示分层错误实现,+1/0 表示坐在你后面的那个
【解决方案2】:

如果您有Option 值,并且您想将它们转换为成功/失败值,您可以使用toLefttoRight 方法将Option 转换为Either

通常Right 代表成功,所以使用o.toRight("error message")Some(value) 转换为Right(value)None 转换为Left("error message")

不幸的是,Scala 在默认情况下无法识别这种右偏,因此您必须跳过一个圈(通过调用.right 方法)才能在理解中巧妙地组合您的Eithers。

def requestBodyAsJson: Option[String] = Some("""{"foo":"bar"}""")

def jsonToWine(json: String): Option[Wine] = sys.error("TODO")

val wineOrError: Either[String, Wine] = for {
  body <- requestBodyAsJson.toRight("Expecting JSON Data").right
  wine <- jsonToWine(body).toRight("Invalid Wine entity").right
} yield wine

【讨论】:

【解决方案3】:

如果你需要一个空值,你可以使用lift Box来代替Either[A,Option[B]],它可以有三个值:

  • Full(有有效结果)
  • Empty(没有结果,但也没有错误)
  • Failure(发生错误)

BoxEither 更灵活,这要归功于rich API。当然,虽然它们是为 Lift 创建的,但您可以在任何其他框架中使用它们。

【讨论】:

    【解决方案4】:

    嗯,这是我尝试使用 Either

    def save() = CORSAction { request =>
      request.body.asJson.map { json =>
        json.asOpt[Wine].map { wine =>
          wine.save.fold(
            errors => JsonBadRequest(errors),
            wine => Ok(toJson(wine).toString)
          )
        }.getOrElse     (JsonBadRequest("Invalid Wine entity"))
      }.getOrElse       (JsonBadRequest("Expecting JSON data"))
    }
    

    wine.save 是这样的:

    def save(wine: Wine): Either[List[Error],Wine] = {
    
      val errors = validate(wine)
      if (errors.length > 0) {
        Left(errors)
      } else {
        DB.withConnection { implicit connection =>
          val newId = SQL("""
            insert into wine (
              name, year, grapes, country, region, description, picture
            ) values (
              {name}, {year}, {grapes}, {country}, {region}, {description}, {picture}
            )"""
          ).on(
            'name -> wine.name, 'year -> wine.year, 'grapes -> wine.grapes,
            'country -> wine.country, 'region -> wine.region, 'description -> wine.description,
            'picture -> wine.picture
          ).executeInsert()
    
          val newWine = for {
            id <- newId;
            wine <- findById(id)
          } yield wine
    
          newWine.map { wine =>
            Right(wine)
          }.getOrElse {
            Left(List(ValidationError("Could not create wine")))
          }
        }
      }
    }
    

    Validate 检查几个先决条件。我仍然需要添加一个 try/catch 来捕获任何 db 错误

    我仍在寻找一种方法来改进整个事情,感觉很冗长符合我的口味......

    【讨论】:

      猜你喜欢
      • 2016-06-20
      • 2014-03-06
      • 2023-03-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-09-25
      • 1970-01-01
      相关资源
      最近更新 更多