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.
Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking or indexing (or even by attribute in the case of namedtuples ). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.
Does a list need to be homogeneous? NO, There is no need to be homogeneous because . List is used to store the collection of values. List can be either homogeneous and heterogeneous.
Lists in ML are homogeneous: a list cannot contain elements of different types. This may be annoying to new ML users, yet lists are not as fundamental as in Lisp, since ML provides a facility for introducing new types allowing the user to define more precisely the data structures needed by the program (cf. chapter 6).
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-Foo
s, 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
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With