scala:如何以功能方式处理验证

时间:2012-08-22 06:17:42

标签: scala error-handling

如果它传递了条件列表,我正在开发一个应该持久保存对象的方法。

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

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

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

也就是说,我希望将其视为选项[T],如果任何验证失败,而不是返回None,则会向我提供错误列表...

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

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

-

哎呀,刚发布了这个问题并发现了<或者

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

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

4 个答案:

答案 0 :(得分:13)

使用scalaz,你有Validation[E, A],就像Either[E, A]一样,但是如果E是一个半群(意思是可以连接的东西,比如列表)而不是多个验证结果的属性可以以保持发生的所有错误的方式组合。

例如,使用Scala 2.10-M6和Scalaz 7.0.0-M2,其中Scalaz有一个名为Either[L, R]的自定义\/[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}"
}

此处resultValidation[NonEmptyList[Throwable], String],包含发生的所有错误(临时传感器错误和/或推特错误或无)或成功消息。然后,您可以切换回\/以方便使用。

注意:Either和Validation之间的区别主要在于验证可以累积错误,但不能flatMap丢失累积的错误,而使用你不能(轻松)累积但可以{{1 (或者在for-comprehension中)并且可能丢失除第一条错误消息之外的所有消息。

关于错误层次结构

我认为这可能对你有用。无论使用scalaz / flatMap / Either / \/,我都意识到入门很容易,但未来还需要一些额外的工作。问题是,如何以有意义的方式从多个错误的函数中收集错误?当然,你可以在任何地方使用ValidationThrowable并且有一个轻松的时间,但听起来不太可用或无法解释。想象一下,得到一个错误列表,如“儿童年龄缺失”::“IO错误读取文件”::“除以零”。

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

List[String]

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

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 <-: e左侧的函数f,因为e: \/是一个Bifunctor。对于\/,您可能有se: scala.Either

这可以通过提供shapeless se.left.map(f)来进一步改进,以便能够绘制漂亮的错误树。

<强>修订

更新:HListIso将失败方面提升为(e: \/).vnel,因此如果我们发生故障,我们至少会出现一次错误(是:或者没有)。

答案 1 :(得分:4)

如果您有Option个值,并且想要将其变为成功/失败值,则可以使用Option或{EithertoLeft变为toRight {1}}方法。

通常Right表示成功,因此请使用o.toRight("error message")Some(value)转换为Right(value),将None转换为Left("error message")

不幸的是,Scala默认情况下不会识别出这种偏见,所以你必须跳过一个环(通过调用.right方法)才能整齐地组成你的Either。 -comprehension。

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

答案 2 :(得分:2)

如果您需要空值,而不是使用Either[A,Option[B]],则可以使用电梯Box,它可以有三个值:

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

BoxEither更灵活。当然,尽管它们是为Lift创建的,但您可以在任何其他框架中使用它们。

答案 3 :(得分:0)

嗯,这是我尝试使用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")))
      }
    }
  }
}

验证检查几个前提条件。我仍然需要添加一个try / catch来捕获任何db错误

我仍然在寻找一种方法来改善整个事情,对我的口味感觉很多...