Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to ensure tuple is homogenous?

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.

like image 915
user Avatar asked Oct 14 '20 15:10

user


People also ask

Is tuple homogeneous?

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 needs to be homogeneous?

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.

Why are lists homogeneous in ML?

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).


1 Answers

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
}
like image 55
user Avatar answered Sep 28 '22 06:09

user