Using Slick with shapeless HList - scala

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

Related

Unzip a query of tuple in Slick+shapeless

I have a Slick (grouping) query returning a tuple of integers which should be then combined using one of SQL aggregation functions. The size of the tuple is not dynamic but I would like to be able to define it in a single place with possibility to change it later. I use shapeless elsewhere in my project so it is natural to use it here as well.
So, let's say we have a tuple type type Agg = (Rep[Int], Rep[Int], Rep[Int]) and a query with type Query[Agg, _, Seq]
I can then define poly's for aggregation like this:
object sum extends (Query[Rep[Int], Int, Seq] -> Rep[Int])(_.sum.getOrElse(0))
object sumAgg extends(Query[Agg, _, Seq] -> Agg)(q => (q.map(_._1), q.map(_._2), q.map(_._3)).map(sum))
But I can't find a way to get rid of explicit tuple unzipping in sumAgg poly. How can I transform a Query of tuple of Int's (aka Agg) into a tuple of queries of Int's?
Let's even simplify this. Supposing I have
val hlist = 1 :: 2 :: 3 :: HNil
val tuple = (4, 5, 6)
hlist.zipWithIndex.map(m)
What would be the definition of m to produce, say, a hlist of (1*4) :: (2*5) :: (3*6) :: HNil? I know I could directly zip hlist with tuple but in this scenario I think I do need to pick tuple elements one by one by their positions.
Try to replace sumAgg with type classes.
import shapeless.{::, HList, HNil, Nat, Poly1, Succ}
import shapeless.nat._
import shapeless.poly.->
import shapeless.syntax.std.tuple._
import shapeless.ops.hlist.Tupler
import shapeless.ops.tuple.{At, Length, Mapper}
import slick.lifted.{Query, Rep, Shape}
import slick.jdbc.PostgresProfile.api._
trait MkHList[Agg <: Product, N <: Nat] {
type Out <: HList
def apply(q: Query[Agg, _, Seq]): Out
}
object MkHList {
type Aux[Agg <: Product, N <: Nat, Out0 <: HList] = MkHList[Agg, N] { type Out = Out0 }
def instance[Agg <: Product, N <: Nat, Out0 <: HList](f: Query[Agg, _, Seq] => Out0): Aux[Agg, N, Out0] = new MkHList[Agg, N] {
override type Out = Out0
override def apply(q: Query[Agg, _, Seq]): Out = f(q)
}
implicit def zero[Agg <: Product]: Aux[Agg, _0, HNil] = instance(_ => HNil)
implicit def succ[Agg <: Product, N <: Nat, A](implicit
tailMkHList: MkHList[Agg, N],
at: At.Aux[Agg, N, Rep[A]],
shape: Shape[_ <: FlatShapeLevel, Rep[A], A, Rep[A]]
): Aux[Agg, Succ[N], Query[Rep[A], A, Seq] :: tailMkHList.Out] =
instance(q => q.map(_.at[N]) :: tailMkHList(q))
}
trait SumAgg[Agg <: Product] {
def apply(q: Query[Agg, _, Seq]): Agg
}
object SumAgg {
implicit def mkSumAgg[Agg <: Product, N <: Nat, L <: HList, Tpl <: Product](implicit
length: Length.Aux[Agg, N],
mkHList: MkHList.Aux[Agg, N, L],
tupler: Tupler.Aux[L, Tpl],
mapper: Mapper.Aux[Tpl, sum.type, Agg]
): SumAgg[Agg] = q => tupler(mkHList(q)).map(sum)
}
def sumAgg[Agg <: Product](q: Query[Agg, _, Seq])(implicit sumAggInst: SumAgg[Agg]): Agg = sumAggInst(q)
type Agg = (Rep[Int], Rep[Int], Rep[Int])
sumAgg(??? : Query[Agg, _, Seq]): Agg
Answering your simplified version:
import shapeless._
import syntax.std.tuple._ // brings implicits for tuple.productElements
// defines your polymorphic mapper
object MulPoly extends Poly1 {
// you're going to need one case for each pair of types that you might face.
// E.g. (Int, Int), (Int, String), (String, String), etc.
implicit val intPairCase: Case.Aux[(Int, Int), Int] = at({ case (a, b) => a * b })
}
val hlist = 1 :: 2 :: 3 :: HNil
val tuple = (4, 5, 6)
val tupleHList = tuple.productElements
>>> tupleHList: Int :: Int :: Int :: shapeless.HNil = 4 :: 5 :: 6 :: HNil
hlist
.zip(tupleHList) // intermediate result here: (1, 4) :: (2, 5) :: (3, 6) :: HNil
.map(MulPoly) // map MulPoly over the HList
>>> res0: Int :: Int :: Int :: shapeless.HNil = 4 :: 10 :: 18 :: HNil
So, essentially, your m is a Poly1, that can map on 2-tuples of types that constitute your hlist and a tuple.
You might want to consult with Type Astronaut's Guide to Shapeless, Section 7 about ploymorphic mapping, and this stackoverflow question about how to get an hlist out of any tuple.

Create "enriched" type from case class with Shapeless in Scala

I have this example code:
import java.util.UUID
import shapeless.LabelledGeneric
import shapeless.record._
import shapeless.syntax.singleton._
object LabelTest extends App {
case class IncomingThing(name: String, age: Int)
case class DatabaseIncomingThing(name: String, age: Int, id: UUID)
val genIncoming = LabelledGeneric[IncomingThing]
val genDatabase = LabelledGeneric[DatabaseIncomingThing]
val thing = IncomingThing("John", 42)
val structuralVersionOfIncomingThing = genIncoming.to(thing)
val updated = genDatabase.from(structuralVersionOfIncomingThing + ('id ->> UUID.randomUUID()))
println(updated) // DatabaseIncomingThing(John,42,a45081f2-4ed5-4d2b-8fd9-4d8986875ed7)
}
Which is nice because I don't have to write the boilerplate that copies over all the fields from IncomingThing to DatabaseIncomingThing. However, I would love not having to maintain both those types since there is a very clear relationship between the two (one has id, the other doesn't).
Is there a way to create a type from a given case class by either adding or removing one field?
I imagine something like
type IncomingThing = withoutField[DatabaseIncomingThing]('id)
Or something to that effect.
Instead of DatabaseIncomingThing
val updated: DatabaseIncomingThing =
genDatabase.from(structuralVersionOfIncomingThing + ('id ->> UUID.randomUUID()))
you can work with raw HList
val updated1: FieldType[Witness.`'name`.T, String] :: FieldType[Witness.`'age`.T, Int] :: FieldType[Witness.`'id`.T, UUID] :: HNil =
structuralVersionOfIncomingThing + ('id ->> UUID.randomUUID())
val updated2: FieldType[Witness.`'name`.T, String] :: FieldType[Witness.`'age`.T, Int] :: HNil =
updated1 - 'id
On type level
implicitly[Remover.Aux[FieldType[Witness.`'name`.T, String] :: FieldType[Witness.`'age`.T, Int] :: FieldType[Witness.`'id`.T, UUID] :: HNil,
Witness.`'id`.T,
(UUID, FieldType[Witness.`'name`.T, String] :: FieldType[Witness.`'age`.T, Int] :: HNil)]]
implicitly[Updater.Aux[
FieldType[Witness.`'name`.T, String] :: FieldType[Witness.`'age`.T, Int] :: HNil,
FieldType[Witness.`'id`.T, UUID],
FieldType[Witness.`'name`.T, String] :: FieldType[Witness.`'age`.T, Int] :: FieldType[Witness.`'id`.T, UUID] :: HNil]]
You can create your type class
trait WithoutField[A, K] {
type Out <: HList
}
object WithoutField {
type Aux[A, K, Out0 <: HList] = WithoutField[A, K] { type Out = Out0 }
def instance[A, K, Out0 <: HList]: Aux[A, K, Out0] = new WithoutField[A, K] { type Out = Out0 }
implicit def mkWithoutField[A, L <: HList, K, T, L1 <: HList](implicit
labelledGeneric: LabelledGeneric.Aux[A, L],
remover: Remover.Aux[L, K, (T, L1)]): Aux[A, K, L1] = instance
}
and use it
def foo[Out <: HList](implicit withoutField: WithoutField.Aux[DatabaseIncomingThing, Witness.`'id`.T, Out]) = {
// now you can use type Out inside
type IncomingThing = Out
???
}

shapeless convert case class to HList and skip all option fields

I have the next class:
case class Foo(a: Option[Int], b: Option[String], c: Option[Double])
as you can see, all fields is optional, i want convert this class into HList or Tuple, like
val f1 = Foo(Some(1) , None, Some(3D))
val f2 = Foo(None, "foo")
val result1 = f1.to[Int::Double::HNil] // => 1::3D
val result2 = f2.to[String::HNil] // "foo"
is it possible, without reflection?
It might be possible to do this with existing type classes in Shapeless (something like NatTRel and RemoveAll), but I'm not 100% sure of that, and this is a case where I'd just write my own type class:
import shapeless._
trait OptionalPieces[L <: HList, S <: HList] {
def apply(l: L): Option[S]
}
object OptionalPieces extends LowPriorityOptionalPieces {
implicit val hnilOptionalPieces: OptionalPieces[HNil, HNil] =
new OptionalPieces[HNil, HNil] {
def apply(l: HNil): Option[HNil] = Some(HNil)
}
implicit def hconsOptionalPiecesMatch[H, T <: HList, S <: HList](implicit
opt: OptionalPieces[T, S]
): OptionalPieces[Option[H] :: T, H :: S] =
new OptionalPieces[Option[H] :: T, H :: S] {
def apply(l: Option[H] :: T): Option[H :: S] = for {
h <- l.head
t <- opt(l.tail)
} yield h :: t
}
}
sealed class LowPriorityOptionalPieces {
implicit def hconsOptionalPiecesNoMatch[H, T <: HList, S <: HList](implicit
opt: OptionalPieces[T, S]
): OptionalPieces[Option[H] :: T, S] =
new OptionalPieces[Option[H] :: T, S] {
def apply(l: Option[H] :: T): Option[S] = opt(l.tail)
}
}
This witnesses that L contains at least all of the elements of S wrapped in Option, in order, and gives you a way to unwrap them at runtime (safely).
We can then define a syntax helper class like this:
implicit class OptionalPiecesSyntax[A, R <: HList](a: A)(implicit
gen: Generic.Aux[A, R]
) {
def to[S <: HList](implicit op: OptionalPieces[gen.Repr, S]): Option[S] =
op(gen.to(a))
}
And then:
scala> val f1 = Foo(Some(1) , None, Some(3D))
f1: Foo = Foo(Some(1),None,Some(3.0))
scala> val f2 = Foo(None, Some("foo"), None)
f2: Foo = Foo(None,Some(foo),None)
scala> val result1 = f1.to[Int :: Double :: HNil]
result1: Option[shapeless.::[Int,shapeless.::[Double,shapeless.HNil]]] = Some(1 :: 3.0 :: HNil)
scala> val result2 = f2.to[String :: HNil]
result2: Option[shapeless.::[String,shapeless.HNil]] = Some(foo :: HNil)
If you really wanted exceptions, you could just call .get in the syntax class, but that seems like a bad idea.

Automatically convert a case class to an extensible record in shapeless?

If I have these two case classes:
case class Address(street : String, zip : Int)
case class Person(name : String, address : Address)
and an instance:
val person = Person("Jane", Address("street address", 12345))
Is there a way in shapeless to automatically convert person to an extensible record?
I am interested in both shallow and deep conversions.
The shallow copy would be something like:
'name ->> "Jane" :: 'address ->> Address("street address", 12345) :: HNil
In the deep conversion, the nested case class also becomes a record:
'name ->> "Jane" :: 'address ->> ('street ->> "street address" :: 'zip ->> 12345 :: HNil) :: HNil
I am also interested in converting records back to case classes.
Suppose we've got the following setup:
import shapeless._, shapeless.labelled.{ FieldType, field }
case class Address(street: String, zip: Int)
case class Person(name: String, address: Address)
val person = Person("Jane", Address("street address", 12345))
type ShallowPersonRec =
FieldType[Witness.`'name`.T, String] ::
FieldType[Witness.`'address`.T, Address] :: HNil
type DeepPersonRec =
FieldType[Witness.`'name`.T, String] ::
FieldType[
Witness.`'address`.T,
FieldType[Witness.`'street`.T, String] ::
FieldType[Witness.`'zip`.T, Int] :: HNil
] :: HNil
Shapeless's LabelledGeneric supports the shallow case directly:
val shallow: ShallowPersonRec = LabelledGeneric[Person].to(person)
Or if you want a generic helper method:
def shallowRec[A](a: A)(implicit gen: LabelledGeneric[A]): gen.Repr = gen.to(a)
val shallow: ShallowPersonRec = shallowRec(person)
And you can go back with from:
scala> val originalPerson = LabelledGeneric[Person].from(shallow)
originalPerson: Person = Person(Jane,Address(street address,12345))
The deep case is trickier, and as far as I know there's no convenient way to do this with the type classes and other tools provided by Shapeless, but you can adapt my code from this question (which is now a test case in Shapeless) to do what you want. First for the type class itself:
trait DeepRec[L] extends DepFn1[L] {
type Out <: HList
def fromRec(out: Out): L
}
And then a low-priority instance for the case where the head of the record doesn't itself have a LabelledGeneric instance:
trait LowPriorityDeepRec {
type Aux[L, Out0] = DeepRec[L] { type Out = Out0 }
implicit def hconsDeepRec0[H, T <: HList](implicit
tdr: Lazy[DeepRec[T]]
): Aux[H :: T, H :: tdr.value.Out] = new DeepRec[H :: T] {
type Out = H :: tdr.value.Out
def apply(in: H :: T): H :: tdr.value.Out = in.head :: tdr.value(in.tail)
def fromRec(out: H :: tdr.value.Out): H :: T =
out.head :: tdr.value.fromRec(out.tail)
}
}
And then the rest of the companion object:
object DeepRec extends LowPriorityDeepRec {
def toRec[A, Repr <: HList](a: A)(implicit
gen: LabelledGeneric.Aux[A, Repr],
rdr: DeepRec[Repr]
): rdr.Out = rdr(gen.to(a))
class ToCcPartiallyApplied[A, Repr](val gen: LabelledGeneric.Aux[A, Repr]) {
type Repr = gen.Repr
def from[Out0, Out1](out: Out0)(implicit
rdr: Aux[Repr, Out1],
eqv: Out0 =:= Out1
): A = gen.from(rdr.fromRec(eqv(out)))
}
def to[A](implicit
gen: LabelledGeneric[A]
): ToCcPartiallyApplied[A, gen.Repr] =
new ToCcPartiallyApplied[A, gen.Repr](gen)
implicit val hnilDeepRec: Aux[HNil, HNil] = new DeepRec[HNil] {
type Out = HNil
def apply(in: HNil): HNil = in
def fromRec(out: HNil): HNil = out
}
implicit def hconsDeepRec1[K <: Symbol, V, Repr <: HList, T <: HList](implicit
gen: LabelledGeneric.Aux[V, Repr],
hdr: Lazy[DeepRec[Repr]],
tdr: Lazy[DeepRec[T]]
): Aux[FieldType[K, V] :: T, FieldType[K, hdr.value.Out] :: tdr.value.Out] =
new DeepRec[FieldType[K, V] :: T] {
type Out = FieldType[K, hdr.value.Out] :: tdr.value.Out
def apply(
in: FieldType[K, V] :: T
): FieldType[K, hdr.value.Out] :: tdr.value.Out =
field[K](hdr.value(gen.to(in.head))) :: tdr.value(in.tail)
def fromRec(
out: FieldType[K, hdr.value.Out] :: tdr.value.Out
): FieldType[K, V] :: T =
field[K](gen.from(hdr.value.fromRec(out.head))) ::
tdr.value.fromRec(out.tail)
}
}
(Note that the DeepRec trait and object must be defined together to be companioned.)
This is messy, but it works:
scala> val deep: DeepPersonRec = DeepRec.toRec(person)
deep: DeepPersonRec = Jane :: (street address :: 12345 :: HNil) :: HNil
scala> val originalPerson = DeepRec.to[Person].from(deep)
originalPerson: Person = Person(Jane,Address(street address,12345))
The to / from syntax for the conversion back to the case class is necessary because any given record could correspond to a very large number of potential case classes, so we need to be able to specify the target type, and since Scala doesn't support partially-applied type parameter lists, we have to break up the operation into two parts (one of which will have its types specified explicitly, while the type parameters for the other will be inferred).

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"