How to ensure tuple is homogenous?

235 views Asked by At

For reasons beyond my control, my method receives inputs in the form of a tuple. This tuple should only contain instances of Foo, i.e., it should look like (Foo, Foo ... Foo) and should not have String or Int inside. I want to check this at compile-time instead of throwing an exception at runtime. How can I achieve this?

Below is the code I currently have, which is not right:

def f(tupleOfFoos: Tuple): Tuple = {
  for (x <- tupleOfFoos) assert(x.isInstanceOf[Foo])
  mapTuple(tupleOfFoos, irrelevantFunction)
}

I am open to using Shapeless or the new features introduced in Dotty/Scala 3.

1

There are 1 answers

0
username On

In Scala 2, with Shapeless, you can do this (Scastie):

def f[T <: Product, H <: HList](tupleOfFoos: T)(
  implicit gen: Generic.Aux[T, H], 
  hev: LiftAll[({type E[T] = Foo =:= T})#E, H]
) = tupleOfFoos

LiftAll ensures there's an instance of Foo =:= X for every X in H, and gen makes sure that T and H are not completely unrelated types.


In Dotty, you can add an evidence parameter with a match type for this:

type Homogenous[H, T <: Tuple] = T match {
    case EmptyTuple => DummyImplicit
    case H *: t => Homogenous[H, t]
    case _ => Nothing
}
    
def f[T <: Tuple](tupleOfFoos: T)(using Homogenous[Foo, T]) = tupleOfFoos

This will allow you to call f((Foo(), Foo(), Foo())) but not f((1, 2, 3)).

Homogenous is a recursive match type with a base case of EmptyTuple. If a tuple is empty, then it's not filled with non-Foos, so the type becomes DummyImplicit, which has an implicit already in scope. Otherwise, we check if it looks like (H, ...) / H *: t, in which case we need to check if the rest of the tuple (t) is also valid. If it doesn't match that second case, we know the tuple is invalid, in which case the result is Nothing, which sane people don't make implicit values of.

If you want to use context bounds, you can make an additional curried type (Scastie):

type Homo2[H] = [T <: Tuple] =>> Homogenous[H, T]

def f[T <: Tuple : Homo2[Foo]](tupleOfFoos: T) = tupleOfFoos

Unfortunately, I haven't been able to get it to work with a single curried type (Scastie):

type Homogenous[H] = [T <: Tuple] =>> T match {
    case EmptyTuple => DummyImplicit
    case H *: t => Homogenous[H][t]
    case _ => Nothing
}