5

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.

David Apltauer
  • 963
  • 2
  • 11
  • 23

3 Answers3

1

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))
idonnie
  • 1,703
  • 12
  • 11
  • Thanks. However this seems rather rigid. the parsing may fail too producing no template at all. Your solution seems bound to one type and its transformations only. See my edit above for what I was hoping for. – David Apltauer Apr 01 '13 at 18:27
1

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.

Community
  • 1
  • 1
sourcedelica
  • 23,940
  • 7
  • 66
  • 74
0

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()))
David Apltauer
  • 963
  • 2
  • 11
  • 23