Search code examples
scalaconstraintsshapelesshlist

Shapeless: own HList constraint using Coproduct


(NOTE: Split from Shapeless: Trying to restrict HList elements by their type )

Question 2 - Own Constraint using Coproduct

What I really wanted to do is to write a new constraint using Coproduct.

trait CPConstraint[L <: HList, CP <: Coproduct] extends Serializable
object CPConstraint {
  import shapeless.ops.coproduct.Selector._

  def apply[L <: HList, CP <: Coproduct](implicit cpc: CPConstraint[L, CP]): CPConstraint[L, CP] = cpc

  type <*<[CP <: Coproduct] = {  // TODO: just invented a symbol ... what would be an appropriate one?
    type λ[L <: HList] = CPConstraint[L, CP]
  }

implicit def hnilCP[HN <: HNil, CP <: Coproduct]: CPConstraint[HN, CP] = new CPConstraint[HN, CP] {}
implicit def hlistCP[H, T <: HList, CP <: Coproduct](implicit ev: coproduct.Selector[CP, H], cpct: CPConstraint[T, CP]): CPConstraint[H :: T, CP] = new CPConstraint[H :: T, CP] {}

}

object testCPConstraint {
  import shapeless.ops.coproduct.Selector._
  import CPConstraint._

  type CPType = Long :+: String :+: CNil

  implicit val selLong = implicitly[Selector[CPType, Long]]
  implicit val selString = implicitly[Selector[CPType, String]]

  def acceptCP[L <: HList : <*<[CPType]#λ](l: L) = true

  val hlLong: ::[Long, HNil] = 1L :: HNil
  val hlString: ::[String, HNil] = "blabla" :: HNil
  val hlMixed: ::[String, ::[Long, HNil]] = "blabla" :: 1L :: HNil
  val hlMixedRev: ::[Long, ::[String, HNil]] = 1L :: "blabla" :: HNil
  val hlInvalid: ::[Double, HNil] = 1.0d :: HNil

  implicit val scpcEmpty: CPConstraint[HNil, CPType] = implicitly[CPConstraint[HNil, CPType]]

  implicit val scpcEmptyLong1: CPConstraint[::[Long,HNil], CPType] = new CPConstraint[::[Long,HNil], CPType] {}

// implicit val scpcEmptyLong2: CPConstraint[hlLong.type, CPType] = new CPConstraint[hlLong.type, CPType] {} // above line would fit the missing implicit - WHY???

  implicit val cpcLong = implicitly[CPConstraint[hlLong.type, CPType]]

  val validEmpty = acceptCP(HNil: HNil)
  val validLong = acceptCP(1l :: HNil)
  val validMixed = acceptCP("blabla" :: 1l :: HNil)

  val invalid = acceptCP(1.0d :: HNil) // should fail due to missing evidence
}

Solution

  • While experimenting ... I got it working:

    import shapeless.ops.coproduct
    import shapeless.{:+:, ::, CNil, Coproduct, HList, HNil}
    
    /**
     * constraint that checks {{{shapeless.HList}}} elements to be of a type contained in a {{{shapeless.Coproduct}}}
     */
    trait CPConstraint[L <: HList, CP <: Coproduct] extends Serializable
    
    object CPConstraint {
      def apply[L <: HList, CP <: Coproduct](implicit cpc: CPConstraint[L, CP]): CPConstraint[L, CP] = cpc
    
      type <*<[CP <: Coproduct] = {
        type λ[L <: HList] = CPConstraint[L, CP]
      }
    
      implicit def hnilCP[HN <: HNil, CP <: Coproduct]: CPConstraint[HN, CP] = new CPConstraint[HN, CP] {}
      implicit def hlistCP[H, T <: HList, CP <: Coproduct](implicit ev: coproduct.Selector[CP, H], cpct: CPConstraint[T, CP]): CPConstraint[H :: T, CP] = new CPConstraint[H :: T, CP] {}
    }
    
    object testCPConstraint {
      import CPConstraint._
    
      type CPType = Long :+: String :+: CNil
      def acceptCP[L <: HList : <*<[CPType]#λ](l: L) = true
    
      val hlLong = 1L :: (HNil: HNil)
      val hlString = "blabla" :: HNil
      val hlMixed = "blabla" :: 1L :: HNil
      val hlMixedRev = 1L :: "blabla" :: HNil
      val hlInvalid = 1.0d :: HNil
    
      val validEmpty = acceptCP(HNil)
      val validEmpty2 = acceptCP(HList())
      val validEmpty3 = acceptCP(HNil: HNil)
      val validString = acceptCP(hlString)
      val validLong = acceptCP(hlLong)
      val validMixed = acceptCP(hlMixed)
      val validMixedRev = acceptCP(hlMixedRev)
      //  val invalid = acceptCP(hlInvalid) // -> fails due to missing evidence (as expected)
    
      implicit val scpcEmpty = implicitly[CPConstraint[HNil, CPType]]
    
      //  implicit val cpcLong = implicitly[CPConstraint[hlLong.type, CPType]]
      // I still don't understand why above line doesn't work, but the following does ???
      implicit val cpcLong2 = implicitly[CPConstraint[::[Long,HNil], CPType]]    
    }