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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With