Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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. 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.

like image 781
David Apltauer Avatar asked Nov 03 '22 01:11

David Apltauer


2 Answers

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))
like image 189
idonnie Avatar answered Nov 15 '22 06:11

idonnie


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.

like image 41
sourcedelica Avatar answered Nov 15 '22 07:11

sourcedelica