3

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?

ponythewhite
  • 617
  • 3
  • 8

2 Answers2

3

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.

Diego Martinoia
  • 4,592
  • 1
  • 17
  • 36
0

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"
ponythewhite
  • 617
  • 3
  • 8