Generic stream builder with shapeless HLists - scala

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)
}

Related

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

Dynamically creating Akka Stream Flows at Runtime

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.

Shapeless HList map after foldRight

A type level foldRight works fine (getLabelWithValues), and a follow-on type level map (getValues) also works fine. If I combine both in one method (getValuesFull), it doesn't work any more though. What is the missing piece?
The full source (with sbt ready to ~run with implicit debug output) is here: https://github.com/mpollmeier/shapeless-playground/tree/8170a5b
case class Label[A](name: String)
case class LabelWithValue[A](label: Label[A], value: A)
val label1 = Label[Int]("a")
val labels = label1 :: HNil
object combineLabelWithValue extends Poly2 {
implicit def atLabel[A, B <: HList] = at[Label[A], (B, Map[String, Any])] {
case (label, (acc, values)) ⇒
(LabelWithValue(label, values(label.name).asInstanceOf[A]) :: acc, values)
}
}
object GetLabelValue extends (LabelWithValue ~> Id) {
def apply[B](labelWithValue: LabelWithValue[B]) = labelWithValue.value
}
val labelsWithValues: LabelWithValue[Int] :: HNil = getLabelWithValues(labels)
// manually mapping it works fine:
val valuesManual: Int :: HNil = labelsWithValues.map(GetLabelValue)
// using a second function with Mapper works fine:
val valuesSecondFn: Int :: HNil = getValues(labelsWithValues)
// error: could not find implicit value for parameter mapper: shapeless.ops.hlist.Mapper.Aux[Main.GetLabelValue.type,WithValues,Values]
// val valuesFull: Int :: HNil = getValuesFull(labels)
def getLabelWithValues[L <: HList, P, WithValues](labels: L)(
implicit folder: RightFolder.Aux[L, (HNil.type, Map[String, Any]), combineLabelWithValue.type, P],
ic: IsComposite.Aux[P, WithValues, _]
): WithValues = {
val state = Map("a" -> 5, "b" -> "five")
val resultTuple = labels.foldRight((HNil, state))(combineLabelWithValue)
ic.head(resultTuple)
}
def getValues[WithValues <: HList, Values <: HList](withValues: WithValues)(
implicit mapper: Mapper.Aux[GetLabelValue.type, WithValues, Values]
): Values = {
withValues.map(GetLabelValue)
}
def getValuesFull[L <: HList, P, WithValues <: HList, Values <: HList](labels: L)(
implicit folder: RightFolder.Aux[L, (HNil.type, Map[String, Any]), combineLabelWithValue.type, P],
ic: IsComposite.Aux[P, WithValues, _],
mapper: Mapper.Aux[GetLabelValue.type, WithValues, Values]
): Values = {
val state = Map("a" -> 5, "b" -> "five")
val resultTuple = labels.foldRight((HNil, state))(combineLabelWithValue)
val withValues: WithValues = ic.head(resultTuple)
withValues.map(GetLabelValue)
}
The issue here is that you're ending up trying to map over an HList where the HNil is statically typed as HNil.type. This doesn't work in general—e.g. in a simplified case like this:
import shapeless._, ops.hlist.Mapper
val mapped1 = Mapper[poly.identity.type, HNil]
val mapped2 = Mapper[poly.identity.type, HNil.type]
mapped1 will compile, but mapped2 won't.
The trick is to change the HNil.type in your RightFolder types to HNil and then to call foldRight with HNil: HNil. This will make everything work just fine.
There are a few other suggestions I'd make (destructure the tuple in place of P instead of using IsComposite, skip the Aux on mapper and return mapper.Out instead of having a Value type parameter, etc.), but they're probably out of the scope of this question.

Using Slick with shapeless HList

Slick's support for HList is generally a great thing. Unfortunately, it comes with its own implementation that does barely provide any useful operations. I'd therefore like to use the shapeless HList instead. This is supposed to be "trivial", but I have no idea how to get this right. Searching the web I found no evidence that somebody managed to accomplish this task.
I assume that it's enough to implement a ProvenShape (as advertised here), but since I fail to understand the concept of Slick's (Proven)Shapes, I did not manage implement this.
I'm basically aiming to boil this
class Users( tag: Tag )
extends Table[Long :: String :: HNil]( tag, "users" )
{
def id = column[Long]( "id", O.PrimaryKey, O.AutoInc )
def email = column[String]( "email" )
override def * = ( id, email ) <>[TableElementType, ( Long, String )](
_.productElements,
hlist => Some( hlist.tupled )
)
}
down to
class Users( tag: Tag )
extends Table[Long :: String :: HNil]( tag, "users" )
{
def id = column[Long]( "id", O.PrimaryKey, O.AutoInc )
def email = column[String]( "email" )
override def * = id :: email :: HNil
}
You hit the nail on the head - if you can produce Shapes for HLists, the rest of Slick's machinery will kick into gear to produce the ProvenShape you need for the default projection.
Here's a bare-bones implementation that allows you to create Tables of HLists:
import scala.annotation.tailrec
import scala.reflect.ClassTag
import shapeless.{ HList, ::, HNil }
import slick.lifted.{ Shape, ShapeLevel, MappedProductShape }
final class HListShape[L <: ShapeLevel, M <: HList, U <: HList : ClassTag, P <: HList]
(val shapes: Seq[Shape[_, _, _, _]]) extends MappedProductShape[L, HList, M, U, P] {
def buildValue(elems: IndexedSeq[Any]) =
elems.foldRight(HNil: HList)(_ :: _)
def copy(shapes: Seq[Shape[_ <: ShapeLevel, _, _, _]]) =
new HListShape(shapes)
def classTag: ClassTag[U] = implicitly
def runtimeList(value: HList): List[Any] = {
#tailrec def loop(value: HList, acc: List[Any] = Nil): List[Any] = value match {
case HNil => acc
case hd :: tl => loop(tl, hd :: acc)
}
loop(value).reverse
}
override def getIterator(value: HList): Iterator[Any] =
runtimeList(value).iterator
def getElement(value: HList, idx: Int): Any =
runtimeList(value)(idx)
}
object HListShape {
implicit def hnilShape[L <: ShapeLevel]: HListShape[L, HNil, HNil, HNil] =
new HListShape[L, HNil, HNil, HNil](Nil)
implicit def hconsShape[L <: ShapeLevel, M1, M2 <: HList, U1, U2 <: HList, P1, P2 <: HList]
(implicit s1: Shape[_ <: ShapeLevel, M1, U1, P1], s2: HListShape[_ <: ShapeLevel, M2, U2, P2]):
HListShape[L, M1 :: M2, U1 :: U2, P1 :: P2] =
new HListShape[L, M1 :: M2, U1 :: U2, P1 :: P2](s1 +: s2.shapes)
}
I've put an implementation on Github here. In principle I think Generic could be brought into the fray to map case classes without the need for <>.

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"