Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 TA (from 0 to N);
  • it should contain one and only one element of type TB.

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.

like image 804
Alban Dericbourg Avatar asked Aug 30 '16 11:08

Alban Dericbourg


1 Answers

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]
       UniqueTB[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.

like image 156
Travis Brown Avatar answered Nov 22 '22 12:11

Travis Brown