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. Perhaps in a monadic style but I am curious about some general OOP pattern too. 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. 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. I did it using HList, application typeclass and a wrapper class Successful/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.
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]
.
For errors return Left(List("error1",...))
and for success return 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. See this project for code to handle error accumulation using Either
.
Alternatively you can use Scalaz Validation. See Method parameters validation in Scala, with for comprehension and monads for a good explanation.
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
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
@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
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
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()))
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.