Search code examples
scalascalazshapelessimplicits

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?


Solution

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