Search code examples
scalashapeless

Enforce upper bound for HList types at compile time


I'm trying to create a generic trait 'Repo' for some types that are subtypes of a trait 'Identifiable'. My plan is to instantiate implementors of 'Repo' by passing a generic TypeTag[HList] that describes the 'Identifiable'-subtypes.

How can I make the compiler guarantee that the types passed in the HList are subtypes of trait 'Identifiable'?

Here's what I've got so far:

    //All types in HList must extend Identifiable, how to enforce that at compile time?
    trait Repo {
      implicit val ltag: TypeTag[L] forSome {type L <: HList}
      ..
    }

    trait Identifiable {
      ..
    }

    case class Person(..) extends Identifiable
    case class Address(..)

    //This should compile
    class MyRepo 
      (implicit val ltag: TypeTag[Person :: HNil])
      extends Repo {
      ..  
    }

    //This should not
    class MyRepo 
      (implicit val ltag: TypeTag[Address :: HNil])
      extends Repo {
      ..  
    }
//HList can contain an unknown number of types

I've seen this question which seems to be related: Type inference on contents of shapeless HList Difference is I don't have an implementation of the HList to work with so not sure how I can calculate the upper bound with types only.


Solution

  • There's a whole set of constrains on HList provided by https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/hlistconstraints.scala.

    The one you're after is probably LUBConstraint. Quoting the documentation:

    Type class witnessing that every element of L is a subtype of B.

    To use, you just need to require implicit evidence of a LUBContraint[L, Identifiable].

    E.g.

    trait Repo[L <: HList] {
      implicit val ltag: TypeTag[L]
      implicit val ev: LUBConstraint[L, Identifiable]
    }
    
    trait Identifiable
    case class Person(name: String) extends Identifiable
    case class Address(street: String)
    
    type P = Person :: HNil
    class MyPersonRepo(implicit
      val ltag: TypeTag[P],
      val ev: LUBConstraint[P, Identifiable]
    ) extends Repo[P]
    
    
    type A = Address :: HNil
    class MyAddressRepo(implicit
      val ltag: TypeTag[A],
      val ev: LUBConstraint[A, Identifiable]
    ) extends Repo[A]
    
    new MyPersonRepo // this works
    new MyAddressRepo // this doesn't
    

    If you are willing to use an abstract class instead of a trait, you can make everything nicer

    abstract class Repo[L <: HList](implicit
      val ltag: TypeTag[L],
      val ev: LUBConstraint[L, Identifiable]
    )
    
    trait Identifiable
    case class Person(name: String) extends Identifiable
    case class Address(street: String)
    
    type P = Person :: HNil
    class MyPersonRepo extends Repo[P]
    
    type A = Address :: HNil
    class MyAddressRepo extends Repo[A]
    

    Now you'll get the error right away when extending the class.