Alban Dericbourg Alban Dericbourg - 1 year ago 59
Scala Question

Constraint on HList: check for single occurrence of a type

I'm trying to add a constraint on an HList (from Shapeless):

  • it should contain any arbitrary number of elements of type
    (from 0 to N);

  • it should contain one and only one element of type

My example has this type hierarchy:

trait T
case class TA extends T
case class TB extends T

To give examples:

  • tb :: HNil
    is valid

  • ta :: tb ::HNil
    is valid

  • ta :: tb :: ta :: HNil
    is valid

  • ta :: HNil
    is invalid

  • HNil
    is invalid

I cannot figure out how to express this as a constraint.

Answer Source

You can do this with a custom type class that witnesses that there's exactly one TB and all the other elements are TA. If you imagine building up this list inductively, you'll see there are two cases you need to handle—either everything you've seen so far is a TA (which we can witness with a ToList[T, TA]) and the current element is a TB, or you've already seen a single TB and the current element is a TA:

import shapeless._, ops.hlist.{ ToList }

trait T
case class TA() extends T
case class TB() extends T

trait UniqueTB[L <: HList] extends DepFn1[L] {
  type Out = TB
  def apply(l: L): TB

object UniqueTB {
  def apply[L <: HList](implicit utb: UniqueTB[L]): UniqueTB[L] = utb
  def getTB[L <: HList](l: L)(implicit utb: UniqueTB[L]): TB = utb(l)

  implicit def firstTB[T <: HList](
    implicit tl: ToList[T, TA]
  ): UniqueTB[TB :: T] = new UniqueTB[TB :: T] {
    def apply(l: TB :: T): TB = l.head

  implicit def afterTB[T <: HList](
    implicit utb: UniqueTB[T]
  ): UniqueTB[TA :: T] = new UniqueTB[TA :: T] {
    def apply(l: TA :: T): TB = utb(l.tail)

And then:

scala> UniqueTB[TB :: HNil]
res0: UniqueTB[shapeless.::[TB,shapeless.HNil]] = UniqueTB$$anon$1@385c6929

scala> UniqueTB[TA :: TB :: HNil]
res1: UniqueTB[shapeless.::[TA,shapeless.::[TB,shapeless.HNil]]] = UniqueTB$$anon$2@682dd97e

scala> UniqueTB[TA :: TB :: TA :: HNil]
res2: UniqueTB[shapeless.::[TA,shapeless.::[TB,shapeless.::[TA,shapeless.HNil]]]] = UniqueTB$$anon$2@5ef48f82

scala> UniqueTB[TB :: HNil]
res3: UniqueTB[shapeless.::[TB,shapeless.HNil]] = UniqueTB$$anon$1@33be241

scala> UniqueTB[TA :: HNil]
<console>:25: error: could not find implicit value for parameter utb: UniqueTB[shapeless.::[TA,shapeless.HNil]]
       UniqueTB[TA :: HNil]

scala> UniqueTB[HNil]
<console>:25: error: could not find implicit value for parameter utb: UniqueTB[shapeless.HNil]

scala> UniqueTB[TB :: TB :: HNil]
<console>:25: error: could not find implicit value for parameter utb: UniqueTB[shapeless.::[TB,shapeless.::[TB,shapeless.HNil]]]
       UniqueTB[TB :: TB :: HNil]

I've given the type class an operation that returns the TB, but if you don't need that you could leave it method-less.