简体   繁体   English

Scala中的成功/失败链模式

[英]Success/failure chain pattern in Scala

I have a workflow like this: 我有这样的工作流程:

parse template -> check consistency
                                    -> check conformance of one template to another
parse template -> check consistency

Either one of those steps may fail. 这些步骤之一可能会失败。 I would like to implement that in Scala, preferably so that the parallel branches get evaluated independently merging both their errors. 我想在Scala中实现它,最好是使并行分支独立合并两个错误而得到评估。 Perhaps in a monadic style but I am curious about some general OOP pattern too. 也许是单调风格,但我也对某些通用的OOP模式感到好奇。 Currently I have multiple variations hardcoded for various actions with the chaining like this 目前,我对这样的链接有多种形式的硬编码,用于各种动作

def loadLeftTemplateAndForth (leftPath : String, rightPath : String) = {
  val (template, errors) = loadTemplate(leftPath)
  if(errors.isEmpty) loadRightTemplateAndForth(template, rightPath)
  else popupMessage("Error.")
}

which I bet must be some kind of antipattern. 我敢打赌一定是某种反模式。 The steps need decoupling from the workflow but I was not able to come up with anything extremely elegant and there must proven ways already. 这些步骤需要从工作流程中分离出来,但是我无法提出任何非常优雅的方法,并且已经有了行之有效的方法。

EDIT: Ok, so I have unsuccessfully tried to implement something like this 编辑:好的,所以我没有成功尝试实现这样的事情

(((parseTemplate(path1) :: HNil).apply(checkConsistency _) :: ((parseTemplate(path2) :: HNil).apply(checkConsistency _)) :: HNil).apply(checkConformance _)

def checkConformance (t1 : Template)(t2 : Template) : Seq[Error]

The functions would then return Success(result) or Failure(errors). 然后,这些函数将返回成功(结果)或失败(错误)。 I was using HLists but got lost in the type inference rules and other issues. 我使用的是HLists,但是迷上了类型推断规则和其他问题。 It seems I was pretty close though. 看来我已经很近了。 For someone knowledgable of this stuff it would probably be a piece of cake. 对于了解这些东西的人来说,这可能只是小菜一碟。

EDIT: I have finally managed to implement this 编辑:我终于设法实现这一点

(parseTemplate("Suc") :: Args).apply(checkConsistency _) :: 
(parseTemplate("Suc") :: Args).apply(checkConsistency _) :: Args)
.apply(checkConformance _)

with some unfornate constraints that each function must return my equivalent of Either and that the error type of applied function must be a subtype of arguments' error type. 具有一些非限定性的约束,每个函数必须返回我的等效值Either,并且应用函数的错误类型必须是参数错误类型的子类型。 I did it using HList, application typeclass and a wrapper class Successful/UnsuccessfulArgList. 我使用HList,应用程序类型类和包装器类Success / UnsuccessfulArgList进行了此操作。

How about this? 这个怎么样?

// Allows conditional invocation of a method
class When[F](fun: F) {
    def when(cond: F => Boolean)(tail: F => F) = 
      if (cond(fun)) tail(fun) else fun
}
implicit def whenever[F](fun: F): When[F] = new When[F](fun)

After that: 之后:

parseTemplate(t1).when(consistent _){ 
  val parsed1 = _
  parseTemplate(t2).when(consistent _){ 
    conforms(parsed1, _) 
  }
}

Create some holder for errors, and pass it around (to parseTemplate, to consistent, to conforms), or use ThreadLocal. 创建一些错误的持有人,然后将其传递(以使parseTemplate,一致,符合)或使用ThreadLocal。

Here is decoupled much more: 这里的解耦更多:

(parseTemplate(t1), parseTemplate(t2))
  .when(t => consistent(t._1) && consistent(t._2)){ t =>
    conforms(t._1, t._2) 
  }

EDIT 编辑

I've ended up with something like this: 我最终得到了这样的东西:

def parse(path: String): Either[
  String,  // error
  AnyRef   // result
] = ?

def consistent(result: Either[String, AnyRef]): Either[
  String,  // error
  AnyRef   // result
] = ?

def conforms(result1: Either[String, AnyRef], result2: Either[String, AnyRef], 
  fullReport: List[Either[
    List[String],  // either list of errors 
    AnyRef         // or result
  ]]): List[Either[List[String], AnyRef]] = ?

( (parse("t1") :: Nil).map(consistent _), 
  (parse("t2") :: Nil).map(consistent _)
).zipped.foldLeft(List[Either[List[String], AnyRef]]())((fullReport, t1t2) =>
  conforms(t1t2._1, t1t2._2, fullReport))

Have your loadTemplate methods return Either[List[String], Template] . 让您的loadTemplate方法返回Either[List[String], Template]

For errors return Left(List("error1",...)) and for success return Right(template) . 对于错误,返回Left(List("error1",...)) ,对于成功,返回Right(template)

Then you can do 那你可以做

type ELT = Either[List[String], Template]

def loadTemplate(path: String): ELT = ...

def loadRightTemplateAndForth(template: Template, rightPath: String): ELT = ...

def loadLeftTemplateAndForth(leftPath: String, rightPath: String): ELT =
  for {
    lt <- loadTemplate(leftPath).right
    rt <- loadRightTemplateAndForth(lt, rightPath).right
  } yield rt

The above is "fail fast", that is, it won't merge errors from the two branches. 上面是“快速失败”的,也就是说,它不会合并来自两个分支的错误。 If the first fails it will return a Left and won't evaluate the second. 如果第一个失败,它将返回Left并且不会评估第二个。 See this project for code to handle error accumulation using Either . 有关使用Either处理错误累积的代码,请参见此项目

Alternatively you can use Scalaz Validation. 或者,您可以使用Scalaz验证。 See Method parameters validation in Scala, with for comprehension and monads for a good explanation. 请参阅Scala中的方法参数验证,以获取理解和monad的详细说明。

So the way I managed to do it is this (It still could use a refinements though - for example so that it constructs sequence of errors with type common to the list errors and function errors): 因此,我设法做到这一点(尽管它仍然可以使用改进,例如,以便构造与列表错误和函数错误通用的类型的错误序列):

HList.scala HList.scala

import HList.::

sealed trait HList [T <: HList[T]] {

  def ::[H1](h : H1) : HCons[H1, T]

}

object HList { 

  type ::[H, T <: HList[T]] = HCons[H, T] 

  val HNil = new HNil{}

}

final case class HCons[H, T <: HList[T]](head: H, tail: T) extends HList[HCons[H, T]] {

  override def ::[H1](h: H1) = HCons(h, this)

  def apply[F, Out](fun : F)(implicit app : HApply[HCons[H, T], F, Out]) = app.apply(this, fun)

  override def toString = head + " :: " + tail.toString

  None
}

trait HNil extends HList[HNil] {
  override def ::[H1](h: H1) = HCons(h, this)
  override def toString = "HNil"
}

HListApplication.scala HListApplication.scala

@implicitNotFound("Could not find application for list ${L} with function ${F} and output ${Out}.")
trait HApply[L <: HList[L], -F, +Out] {
  def apply(l: L, f: F): Out
}

object HApply {

  import HList.::

  implicit def happlyLast[H, Out] = new HApply[H :: HNil, H => Out, Out] {
    def apply(l: H :: HNil, f: H => Out) = f(l.head)
  }

  implicit def happlyStep[H, T <: HList[T], FT, Out](implicit fct: HApply[T, FT, Out]) = new HApply[H :: T, H => FT, Out] {
    def apply(l: H :: T, f: H => FT) = fct(l.tail, f(l.head))
  }

}

ErrorProne.scala ErrorProne.scala

sealed trait ErrorProne[+F, +S]

case class Success [+F, +S] (result : S) extends ErrorProne[F, S]

case class Failure [+F, +S] (errors : Seq[F]) extends ErrorProne[F, S]

ArgList.scala ArgList.scala

import HList.::
import HList.HNil

sealed trait ArgList [E, L <: HList[L]] {

  def apply[F, S](fun : F)(implicit app : HApply[L, F, ErrorProne[E, S]]) 
  : ErrorProne[E, S]

  def :: [A, E1 <: EX, EX >: E] (argument : ErrorProne[E1, A]) : ArgList[EX, A :: L]

}

case class SuccessArgList [E, L <: HList[L]] (list : L) extends ArgList[E, L] {

  def apply[F, S](fun : F)(implicit app : HApply[L, F, ErrorProne[E, S]]) 
  : ErrorProne[E, S] = app.apply(list, fun)

  override def :: [A, E1 <: EX, EX >: E] (argument : ErrorProne[E1, A]) : ArgList[EX, A :: L] = argument match {
    case Success(a) => SuccessArgList(a :: list)
    case Failure(e) => FailureArgList(e)
  }

}

case class FailureArgList [E, L <: HList[L]] (errors : Seq[E]) extends ArgList[E, L] {

  def apply[F, S](fun : F)(implicit app : HApply[L, F, ErrorProne[E, S]]) 
  : ErrorProne[E, S] = Failure(errors)

  override def :: [A, E1 <: EX, EX >: E] (argument : ErrorProne[E1, A]) : ArgList[EX, A :: L] = argument match {
    case Success(a) => FailureArgList(errors)
    case Failure(newErrors) => FailureArgList(Seq[EX]() ++ errors ++ newErrors)
  }

}

object Args {

  def :: [E1, A] (argument : ErrorProne[E1, A]) : ArgList[E1, A :: HNil] = argument match {
    case Success(a) => SuccessArgList(a :: HNil)
    case Failure(e) => FailureArgList(e)
  }

}

Usage 用法

val result = ((parseTemplate("Suc") :: Args).apply(checkConsistency _) :: 
              (parseTemplate("Suc") :: Args).apply(checkConsistency _) :: Args)
              .apply(checkConformance _)

trait Err
case class Err1 extends Err
case class Err2 extends Err
case class Err3 extends Err

def parseTemplate(name : String) : ErrorProne[Err, Int] = if(name == "Suc") Success(11) else Failure(Seq(Err1()))

def checkConsistency(value : Int) : ErrorProne[Err2, Double] = if(value > 10) Success(0.3) else Failure(Seq(Err2(), Err2()))

def checkConformance(left : Double) (right : Double) : ErrorProne[Err3, Boolean] = 
    if(left == right) Success(true) else Failure(Seq(Err3()))

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM