Dynamically creating Akka Stream Flows at Runtime - scala

I'm currently trying to dynamically create Akka Stream graph definitions at runtime. The idea being that users will be able to define flows interactively and attach them to existing/running BroadcastHubs. This means I don't know which flows or even how many flows will be used at compile time.
Unfortunately, I'm struggling with generics/type erasure. Frankly, I'm not even sure what I'm attempting to do is possible on the JVM.
I have a function that will return an Akka Streams Flow representing two connected Flows. It uses Scala's TypeTags to get around type erasure. If the output type of the first flow is the same as the input type of the second flow, it can be successfully connected. This works just fine.
import akka.NotUsed
import akka.stream.FlowShape
import akka.stream.scaladsl.GraphDSL.Implicits._
import akka.stream.scaladsl.{Flow, GraphDSL}
import scala.reflect.runtime.universe._
import scala.util.{Failure, Success, Try}
def connect[A: TypeTag, B: TypeTag, C: TypeTag, D: TypeTag](a: Flow[A, B, NotUsed],
b: Flow[C, D, NotUsed]): Try[Flow[A, D, NotUsed]] = {
Try {
if (typeOf[B] =:= typeOf[C]) {
val c = b.asInstanceOf[Flow[B, D, NotUsed]]
Flow.fromGraph {
GraphDSL.create(a, c)((m1, m2) => NotUsed.getInstance()) { implicit b =>
(s1, s2) =>
s1 ~> s2
FlowShape(s1.in, s2.out)
}
}
}
else
throw new RuntimeException(s"Connection failed. Incompatible types: ${typeOf[B]} and ${typeOf[C]}")
}
}
So If I have Flow[A,B] and Flow[C,D], the result would be Flow[A,D] assuming that B and C are the same type.
I also have function that attempts to merge/reduce a List of Flows down to a single Flow. Lets assume that this list is derived from a list of flow definitions from a file or web request.
def merge(fcs: List[Flow[_, _, NotUsed]]): Try[Option[Flow[_, _, NotUsed]]] = {
fcs match {
case Nil => Success(None)
case h :: Nil => Success(Some(h))
case h :: t =>
val n = t.head
connect(h, n) match {
case Success(fc) => merge(fc :: t)
case Failure(e) => Failure(e)
}
}
}
Unfortunately, since the Flows are stored inside a List, due to type erasure on standard Lists, I lose all of the type information and therefore am unable to connect the Flows at runtime. Here's an example:
def flowIdentity[A]() = Flow.fromFunction[A, A](x => x)
def flowI2S() = Flow.fromFunction[Int, String](_.toString)
val a = flowIdentity[Int]()
val b = flowIdentity[Int]()
val c = flowI2S()
val d = flowIdentity[String]()
val fcs: List[Flow[_, _, NotUsed]] = List(a, b, c, d)
val y = merge(fcs)
This results in the exception:
Failure(java.lang.RuntimeException: Connection failed. Incompatible types _$4 and _$3)
I've been looking into Miles Sabin'sShapeless, and thought I might be able to use HLists to retain type information. Unfortunately, that seems to work only if I know the individual types and length of the list at compile time. If I upcast a specific HList to just HList, it looks like I lose the type information again.
val fcs: HList = a :: b :: c :: d :: HNil
So my question is... is this even possible? Is there a way to do this with Shapeless generics magic (preferably without the need to use specific non-existential type extractors)? I'd like to find as generic a solution as possible, and any help would be appreciated.
Thanks!

I know this is an old post. As I had some time I gave it a try. Not sure this is exactly the solution, but I thought would post and get suggestions.
type FlowN[A, B] = Flow[A, B, NotUsed]
trait FlowMerger[L <: HList] {
type A
type D
def merge(flow: L): Option[FlowN[A, D]]
}
object FlowMerger extends LowPriorityImplicits {
def apply[L <: HList](v: L)(implicit ev: FlowMerger[L]): Option[FlowN[ev.A, ev.D]] = ev.merge(v)
type Aux[L <: HList, A1, D1] = FlowMerger[L] {
type A = A1
type D = D1
}
implicit def h1Instance[A1, D1]: FlowMerger.Aux[FlowN[A1, D1] :: HNil, A1, D1] = new FlowMerger[FlowN[A1, D1] :: HNil] {
override type A = A1
override type D = D1
override def merge(flow: FlowN[A1, D1] :: HNil): Option[FlowN[A, D]] = Option(flow.head)
}
}
trait LowPriorityImplicits {
implicit def hMulInstance[A1, B1, D1, E1, F1, L <: HList, T <: HList, T1 <: HList]
(implicit
isHC1: IsHCons.Aux[L, FlowN[A1, B1], T],
isHC2: IsHCons.Aux[T, FlowN[E1, F1], T1],
lx: Lazy[FlowMerger[T]],
typeableB: Lazy[Typeable[B1]],
typeableE: Lazy[Typeable[E1]]
): FlowMerger.Aux[L, A1, D1] = {
new FlowMerger[L] {
override type A = A1
override type D = D1
override def merge(flow: L): Option[FlowN[A, D]] = {
if (typeableB.value == typeableE.value) {
lx.value.merge(isHC1.tail(flow)).map(t => isHC1.head(flow) via t.asInstanceOf[FlowN[B1, D]])
} else None
}
}
}
}
You can use it as:
FlowMerger(fcs).map(flow => Source(List(1, 2, 3)) via flow runForeach println)

As you already noticed, the reason it didn't work was that the list erases the types you had. Therefore it is impossible.
If you know all of the types that can be used as intermediate types, you can solve that by adding a resolving function. Adding such a function will also simplify your connect method. I'll add a code snippet. I hope it will be clear.
def flowIdentity[A]() = Flow.fromFunction[A, A](x => x)
def flowI2S() = Flow.fromFunction[Int, String](_.toString)
def main(args: Array[String]): Unit = {
val idInt1 = flowIdentity[Int]()
val idInt2 = flowIdentity[Int]()
val int2String = flowI2S()
val idString = flowIdentity[String]()
val fcs = List(idInt1, idInt2, int2String, idString)
val source = Source(1 to 10)
val mergedGraph = merge(fcs).get.asInstanceOf[Flow[Int, String, NotUsed]]
source.via(mergedGraph).to(Sink.foreach(println)).run()
}
def merge(fcs: List[Flow[_, _, NotUsed]]): Option[Flow[_, _, NotUsed]] = {
fcs match {
case Nil => None
case h :: Nil => Some(h)
case h :: t =>
val n = t.head
val fc = resolveConnect(h, n)
merge(fc :: t.tail)
}
}
def resolveConnect(a: Flow[_, _, NotUsed], b: Flow[_, _, NotUsed]): Flow[_, _, NotUsed] = {
if (a.isInstanceOf[Flow[_, Int, NotUsed]] && b.isInstanceOf[Flow[Int, _, NotUsed]]) {
connectInt(a.asInstanceOf[Flow[_, Int, NotUsed]], b.asInstanceOf[Flow[Int, _, NotUsed]])
} else if (a.isInstanceOf[Flow[_, String, NotUsed]] && b.isInstanceOf[Flow[String, _, NotUsed]]) {
connectString(a.asInstanceOf[Flow[_, String, NotUsed]], b.asInstanceOf[Flow[String, _, NotUsed]])
} else {
throw new UnsupportedOperationException
}
}
def connectInt(a: Flow[_, Int, NotUsed], b: Flow[Int, _, NotUsed]): Flow[_, _, NotUsed] = {
a.via(b)
}
def connectString(a: Flow[_, String, NotUsed], b: Flow[String, _, NotUsed]): Flow[_, _, NotUsed] = {
a.via(b)
}
p.s
There is another bug hiding there, of an endless loop. When calling the merge recursion, the first element should be dropped, as it was already merged into the main flow.

Related

Adding custom collection operations in scala 2.13 to arbitrary collections of specific types

Note - the operation described below now exists in the standard library as partitionMap but I believe it's still a valid question as to how to achieve more general ends
Question regarding scala 2.13 - how do I consume/construct collections of specific types when adding custom collections operations where I need to restrict the element types of the input collections? e.g. how do I define:
def split[CC[_], A, B](coll: CC[Either[A, B]]): (CC[A], CC[B])
Following the documentation I've managed to achieve this as follows:
import collection.generic.IsIterable
import scala.collection.{BuildFrom, Factory}
class SplitOperation[Repr, S <: IsIterable[Repr]](coll: Repr, itr: S) {
def split[A, B, AS, BS](
implicit bfa: BuildFrom[Repr, A, AS],
bfb: BuildFrom[Repr, B, BS],
ev: itr.A =:= Either[A, B]): (AS, BS) = {
val ops = itr(coll)
val as = bfa.fromSpecific(coll)(ops.iterator.map(ev).collect { case Left(a) => a })
val bs = bfb.fromSpecific(coll)(ops.iterator.map(ev).collect { case Right(b) => b })
(as, bs)
}
}
implicit def SplitOperation[Repr](coll: Repr)(implicit itr: IsIterable[Repr]): SplitOperation[Repr, itr.type] =
new SplitOperation(coll, itr)
However, I need to supply types at the use-site otherwise I get diverging implicit expansion.
scala> List(Left("bah"), Right(1), Left("gah"), Right(2), Right(3))
res1: List[scala.util.Either[String,Int]] = List(Left(bah), Right(1), Left(gah), Right(2), Right(3))
scala> res1.split
^
error: diverging implicit expansion for type scala.collection.BuildFrom[List[scala.util.Either[String,Int]],A,AS]
But the following works:
scala> res1.split[String, Int, List[String], List[Int]]
res4: (List[String], List[Int]) = (List(bah, gah),List(1, 2, 3))
EDIT
class SplitOperation[X, CC[_], S <: IsIterable[CC[X]]](coll: CC[X], itr: S) {
def split[A, B](implicit bfa: BuildFrom[CC[X], A, CC[A]], bfb: BuildFrom[CC[X], B, CC[B]], ev: itr.A =:= Either[A, B]): (CC[A], CC[B]) = {
val ops = itr(coll)
val as = bfa.fromSpecific(coll)(ops.iterator.map(ev).collect { case Left(a) => a })
val bs = bfb.fromSpecific(coll)(ops.iterator.map(ev).collect { case Right(b) => b })
(as, bs)
}
}
implicit def SplitOperation[A, B, CC[_]](coll: CC[Either[A, B]])(implicit itr: IsIterable[CC[Either[A, B]]]): SplitOperation[Either[A, B], CC, itr.type] =
new SplitOperation(coll, itr)
Gives me a slight improvement. Now I only need to provide type parameters A and B at the call site:
scala> l.split[String, Int]
res2: (List[String], List[Int]) = (List(bah, gah),List(1, 2))
This seems to work:
class SplitOperation[A, B, CC[_], S <: IsIterable[CC[Either[A, B]]]](coll: CC[Either[A, B]], itr: S) {
def split(implicit bfa: BuildFrom[CC[Either[A, B]], A, CC[A]], bfb: BuildFrom[CC[Either[A, B]], B, CC[B]], ev: itr.A =:= Either[A, B]): (CC[A], CC[B]) = {
val ops = itr(coll)
val as = bfa.fromSpecific(coll)(ops.iterator.map(ev).collect { case Left(a) => a })
val bs = bfb.fromSpecific(coll)(ops.iterator.map(ev).collect { case Right(b) => b })
(as, bs)
}
}
implicit def SplitOperation[A, B, CC[_]](coll: CC[Either[A, B]])(implicit itr: IsIterable[CC[Either[A, B]]]): SplitOperation[A, B, CC, itr.type] =
new SplitOperation(coll, itr)
In your case you don’t want to abstract over the “kind” of the collection type constructor (CC[_] vs CC[_, _], etc.), you always use the CC[_] kind, so you don’t need to use IsIterable.
I think it is also not necessary to support “Sorted” collections (eg, SortedSet) because there is no Ordering instance for Either, so you don’t need to use BuildFrom.
implicit class SplitOperation[A, B, CC[X] <: IterableOps[X, CC, CC[X]]](coll: CC[Either[A, B]]) {
def split: (CC[A], CC[B]) = {
val as = coll.iterableFactory.from(coll.iterator.collect { case Left(a) => a })
val bs = coll.iterableFactory.from(coll.iterator.collect { case Right(b) => b })
(as, bs)
}
}
https://scastie.scala-lang.org/64QxHwteQN2i3udSxCa3yw

writing generic function using method name

I am trying to write the following method:
case class Config2(repoName: String)
def buildAction[A, M, R <: HList]()
(implicit
gen: Generic.Aux[Config2, R],
mod: Modifier.Aux[R, M, A, A, R])
: (A, Config2) => Config2 = {
(arg: A, c: Config2) => {
val rec = mod.apply(gen.to(c), _ => arg)
gen.from(rec)
}
}
When trying to use it with:
buildAction[String, Witness.`'repoName`.T, String :: HList]()
I get an error:
could not find implicit value for parameter gen: shapeless.Generic.Aux[com.advancedtelematic.tuf.cli.Cli.Config2,shapeless.::[String,shapeless.HList]]
[error] val _ = buildAction[String, Witness.`'repoName`.T, String :: HList]()
am I missing some import here?
Second question is, can I somehow rewrite this signature so I don't don't have to specify all the types? In practive the Config2 type takes a long list of fields so it's not pratical to write this all the time
Update:
I simplified this to the following:
val CGen = LabelledGeneric[Config]
def buildAction[A, M]()
(implicit mod: Modifier.Aux[CGen.Repr, M, A, A, CGen.Repr])
: (A, Config) => Config = {
(arg: A, c: Config) => {
val rec = mod.apply(CGen.to(c), _ => arg)
CGen.from(rec)
}
}
Which allows me to just write:
buildAction[String, Witness.`'repoName`.T]()
But I still have to specify that Witness. Is the a way I could write buildAction[String]("repoName") and have some method provide the Witness implicitly?
Update: the following works!
val CGen = LabelledGeneric[Config]
def buildAction[A](witness: Witness)
(implicit mod: Modifier.Aux[CGen.Repr, witness.T, A, A, CGen.Repr]):
(A, Config) => Config = {
(arg: A, c: Config) => {
val rec = mod.apply(CGen.to(c), _ => arg)
CGen.from(rec)
}
}
buildAction[RepoName]('repoName)
am I missing some import here?
No, it's probably just that String :: HList must be String :: HNil
Second question is, can I somehow rewrite this signature so I don't don't have to specify all the types?
You can use a trick known as kinda-curried type parameters:
object buildAction {
class PartiallyApplied[A, M] {
def apply[R <: HList]()(implicit ...)
}
def apply[A, M] = new PartiallyApplied[A, M]
}
Used as
buildAction[String, Witness.`'foo`.T]()
Also, since your code mentions field name, you probably want LabelledGeneric in conjunction with ops.record.Updater

Generic stream builder with shapeless HLists

I have a simple builder which accepts a Source and Sink's from Akka streams and on compile time verifies that a method executed on those has types matching source and sinks.
class EventProcessorTask(config: EventProcessorConfig =
EventProcessorConfig()) {
def source[In, MatIn](source: Source[In, MatIn]): SourcedTask[In, MatIn] = new SourcedTask[In, MatIn](source, config)
}
class SourcedTask[In, MatIn](source: Source[In, MatIn], config: EventProcessorConfig) {
def withPartitioning[Id](partitioningF: In => Id): SourcedTaskWithPartitioning[In, MatIn, Id] =
new SourcedTaskWithPartitioning[In, MatIn, Id](source, partitioningF, config)
}
class SourcedTaskWithPartitioning[In, MatIn, Id](source: Source[In, MatIn], partitioningF: In => Id, config: EventProcessorConfig) {
def withSink[Out, T](sink: Sink[Out, T]): WiredTask[In, MatIn, Out :: HNil, Id, Sink[Out, T] :: HNil] =
new WiredTask[In, MatIn, Out :: HNil, Id, Sink[Out, T] :: HNil](source, sink :: HNil, partitioningF, config)
}
class WiredTask[In, MatIn, L <: HList, Id, SinksTypes <: HList](
source: Source[In, MatIn],
sinks: SinksTypes,
partitioningF: In => Id,
config: EventProcessorConfig
) {
def withSink[Out, T](sink: Sink[Out, T]): WiredTask[In, MatIn, Out :: L, Id, Sink[Out, T] :: SinksTypes] =
new WiredTask[In, MatIn, Out :: L, Id, Sink[Out, T] :: SinksTypes](
source, sink :: sinks, partitioningF, config
)
def execute[N <: Nat, P <: Product, F, R <: HList, SinksRev <: HList]
( executionMethod: In => Future[P])(
implicit generic: Generic.Aux[P, R],
rev: Reverse.Aux[L, R],
sinksRev: Reverse.Aux[SinksTypes, SinksRev],
executionContext: ExecutionContext,
l: Length.Aux[SinksRev, N]
): Unit = {
val sinksReversed = sinksRev(sinks)
// val sinksLength= sinksReversed.length.toInt
}
}
The code above compiles but when I try to build a Broadcast for the Sinks I can't even get the size of the list (commented out code). The next step would be to match all Sinks in SinksRev to corresponding type from P which would allow me to send messages produced by executionMethod which returns a tuple to Sinks corresponding to P types.
I.e.
new EventProcessorTask()
.source(Source.single("str"))
.withPartitioning(r => 1)
.withSink(Sink.head[Long])
.withSink(Sink.foreach((s: String) => ()))
.execute(
in => Future.successful((null.asInstanceOf[Long], null.asInstanceOf[String]))
)
Long should go to first Sink and String to second one.
Any help would be much appreciated. I might be doing something very wrong in here but the concept seemed nice at the time I started working on this (now not so much). Either way I would like to understand what I am missing in here.
To sum up, the questions are:
1. Why can't I get Int representation of SinksRev size?
2. How to match Sinks from SinksRev to corresponding elements in P to build a Broadcast based GraphShape?
I'm into shapeless and akka streams recently myself, so I decided to give this question a try. Your case seems pretty complicated, so I took what I understood, simplified it a bit a came out with code that seems to do something similar to what you probably want. I still cannot calculate the length but since it's a builder then += 1 could suffice.
Here's the result with Options instead of sinks. It takes list of Options and applies function to the content of options. As I mentioned, I simplified the case.
import shapeless._
object Shapes extends App {
import ops.function._
import syntax.std.function._
case class Thing[Types <: HList, Out] private(sources: List[Option[_]]) {
def withOption[T](o: Option[T]) = Thing[T :: Types, Out](o :: sources)
def withOutput[T] = Thing[Types, T](sources)
def apply[F](f: F)(implicit fp: FnToProduct.Aux[F, Types => Out]) = {
val a: Types = sources.foldLeft[HList](HNil)((m, v) ⇒ v.get :: m ).asInstanceOf[Types]
f.toProduct(a)
}
}
object Thing {
def withOption[T](o: Option[T]) = Thing[T :: HNil, AnyVal](o :: Nil)
}
val r = Thing
.withOption(Some(1))
.withOption(Some(2))
.withOption(Some(3))
.withOutput[Unit]
.apply {
(x: Int, y: Int, z: Int) ⇒ println(x + y + z)
}
println(r)
}

Clean up signatures with long implicit parameter lists

Is there an elegant solution to somehow clean up implicit parameter lists making signatures more concise?
I have code like this:
import shapeless._
import shapeless.HList._
import shapeless.ops.hlist._
import shapeless.poly._
trait T[I, O] extends (I => O)
trait Validator[P]
object Validator{
def apply[P] = new Validator[P]{}
}
object valid extends Poly1 {
implicit def caseFunction[In, Out] = at[T[In, Out]](f => Validator[In])
}
object isValid extends Poly2 {
implicit def caseFolder[Last, New] = at[Validator[Last], T[Last, New]]{(v, n) => Validator[New]}
}
object mkTask extends Poly1 {
implicit def caseT[In, Out] = at[T[In, Out]](x => x)
implicit def caseFunction[In, Out] = at[In => Out](f => T[In, Out](f))
}
object Pipeline {
def apply[H <: HList, Head, Res, MapRes <: HList](steps: H)
(implicit
mapper: Mapper.Aux[mkTask.type,H, MapRes],
isCons: IsHCons.Aux[MapRes, Head, _],
cse: Case.Aux[valid.type, Head :: HNil, Res],
folder: LeftFolder[MapRes, Res, isValid.type]
): MapRes = {
val wrapped = (steps map mkTask)
wrapped.foldLeft(valid(wrapped.head))(isValid)
wrapped
}
}
// just for sugar
def T[I, O](f: I => O) = new T[I, O] {
override def apply(v1: I): O = f(v1)
}
Pipeline(T((x:Int) => "a") :: T((x:String) => 5) :: HNil) // compiles OK
Pipeline(((x:Int) => "a") :: ((x:String) => 5) :: HNil) // compiles OK
// Pipeline("abc" :: "5" :: HNil) // doesn't compile
// can we show an error like "Parameters are not of shape ( _ => _ ) or T[_,_]"?
// Pipeline(T((x: Int) => "a") :: T((x: Long) => 4) :: HNil) // doesn't compile
// can we show an error like "Sequentiality constraint failed"?
And I also want to add a couple of implicit params necessary for the library's functionality (to the Pipeline.apply method), but the signature is already huge. I am worried about the ease of understanding for other developers - is there a "best practice" way to structure these params?
Edit: What I mean is the implicit parameters fall into different categories. In this example: mapper ensures proper content types, isCons, cse and folder ensure a sequential constraint on input, and I would like to add implicits representing "doability" of the business logic. How should they be grouped, is it possible to do in a readable format?
Edit2: Would it be possible to somehow alert the library's user, as to which constraint is violated? E.g. either the types in the HList are wrong, or the sequentiality constraint is not held, or he lacks the proper "business logic" implicits?
My suggestion was to use an implict case class that contains that configuration:
case class PipelineArgs(mapper: Mapper.Aux[mkTask.type,H, MapRes] = DEFAULTMAPPER,
isCons: IsHCons.Aux[MapRes, Head, _] = DEFAULTISCON,
cse: Case.Aux[valid.type, Head :: HNil, Res] = DEFAULTCSE,
folder: LeftFolder[MapRes, Res, isValid.type] = DEFAULTFOLDER) {
require (YOUR TESTING LOGIC, YOUR ERROR MESSAGE)
}
object Pipeline {
def apply[H <: HList, Head, Res, MapRes <: HList](steps: H)
(implicit args:PipelineArgs) = {
val wrapped = (steps map mkTask)
wrapped.foldLeft(valid(wrapped.head))(isValid)
wrapped
}
It doesn't help much w.r.t. clarity (but don't worry, I have seen worse), but it helps at notifying the user he's messing up at the creation of the args instance as you can a) put default values to the missing arguments in the CClass constructor b) put a number of "require" clauses.
Thanks to #Diego's answer, I have come up with the following code which works quite nicely:
import scala.annotation.implicitNotFound
import shapeless._
import shapeless.HList._
import shapeless.ops.hlist._
import shapeless.poly._
trait T[I, O] extends (I => O)
trait Validator[P]
object Validator{
def apply[P] = new Validator[P]{}
}
object valid extends Poly1 {
implicit def caseFunction[In, Out] = at[T[In, Out]](f => Validator[In])
}
object isValid extends Poly2 {
implicit def caseFolder[Last, New] = at[Validator[Last], T[Last, New]]{(v, n) => Validator[New]}
}
object mkTask extends Poly1 {
implicit def caseT[In, Out] = at[T[In, Out]](x => x)
implicit def caseFunction[In, Out] = at[In => Out](f => T[In, Out](f))
}
#implicitNotFound("Type constraint violated, elements must be of shape: (_ => _) or T[_, _]")
case class PipelineTypeConstraint[X, H <: HList, MapRes <: HList]
(
mapper: Mapper.Aux[X,H, MapRes]
)
implicit def mkPipelineTypeConstraint[X, H <: HList, MapRes <: HList]
(implicit mapper: Mapper.Aux[X,H, MapRes]) = PipelineTypeConstraint(mapper)
#implicitNotFound("Sequentiality violated, elements must follow: _[A, B] :: _[B, C] :: _[C, D] :: ... :: HNil")
case class PipelineSequentialityConstraint[Head, CRes, MapRes<: HList, ValidT, IsValidT]
(
isCons: IsHCons.Aux[MapRes, Head, _ <: HList],
cse: Case.Aux[ValidT, Head :: HNil, CRes],
folder: LeftFolder[MapRes, CRes, IsValidT]
)
implicit def mkPipelineSequentialityConstraint[Head, CRes, MapRes <: HList, ValidT, IsValidT]
(implicit isCons: IsHCons.Aux[MapRes, Head, _ <: HList],
cse: Case.Aux[ValidT, Head :: HNil, CRes],
folder: LeftFolder[MapRes, CRes, IsValidT]) = PipelineSequentialityConstraint(isCons, cse, folder)
object Pipeline {
def apply[H <: HList, Head, CRes, MapRes <: HList](steps: H)
(implicit
typeConstraint: PipelineTypeConstraint[mkTask.type, H, MapRes],
sequentialityConstraint: PipelineSequentialityConstraint[Head, CRes, MapRes, valid.type, isValid.type]
): MapRes = {
implicit val mapper = typeConstraint.mapper
implicit val isCons = sequentialityConstraint.isCons
implicit val cse = sequentialityConstraint.cse
implicit val folder = sequentialityConstraint.folder
val wrapped = (steps map mkTask)
wrapped.foldLeft(valid(wrapped.head))(isValid)
wrapped
}
}
// just for sugar
def T[I, O](f: I => O) = new T[I, O] {
override def apply(v1: I): O = f(v1)
}
Pipeline(T((x:Int) => "a") :: T((x:String) => 5) :: HNil) // compiles OK
Pipeline(((x:Int) => "a") :: ((x:String) => 5) :: HNil) // compiles OK
Pipeline(5 :: "abc" :: HNil)
// error = "Type constraint violated, elements must be of shape: (_ => _) or T[_, _]
Pipeline(T((x: Int) => "a") :: T((x: Long) => 4) :: HNil)
// error = "Sequentiality violated, elements must follow: (_[A, B] :: _[B, C] :: _[C, D] :: ... :: HNil"

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