Question on "reverse"-matching tuples

Let’s assume I have a declaration like

type TupleOf[Item, Size <: Int] <: Tuple = Size match
  case 0 => EmptyTuple
  case 1 => Item *: EmptyTuple
  case 2 => Item *: Item *: EmptyTuple

(or even a more generic one

type TupleOf[Item, Size <: Int] <: Tuple = Size match
  case 0 => EmptyTuple
  case compiletime.ops.int.S[prevSize] => Item *: TupleOf[Item, prevSize]

though that doesn’t matter for this question).

This works:

def f(a: TupleOf[String, 2]) = ()
f(("hello", "world"))

This also works:

def g(a: TupleOf[?, 2]) = ()
g(("hello", "world"))

This DOES NOT:

def h(a: TupleOf[String, ?]) = ()
h(("hello", "world"))

The message is:

[E007] Type Mismatch Error:
h(("hello", "world"))
  ^^^^^^^^^^^^^^^^^^
  Found:    (String, String)
  Required: TupleOf[String, ?]

  Note: a match type could not be fully reduced:

    trying to reduce  TupleOf[String, ?]
    failed since selector Any
    does not match  case (0 : Int) => EmptyTuple
    and cannot be shown to be disjoint from it either.
    Therefore, reduction cannot advance to the remaining cases

So, IIUC, it can’t prove that (String, String) is disjoint from EmptyTuple.

The first question: What is the status of this thing?

  • Is it by design?
  • Or is it considered as a bug (a temporary state, a to-do) of the compiler?

The second question is: Are there some suggested workarounds?

Specifically: is there a way to specify an arbitrary-sized tuple with items of a predefined base type without using implicits?

I understand that I can do

trait RequiredBase

def myMethod[T <: Tuple](a: T)(using
  Tuple.Union[T] <:< RequiredBase
): T = ???

But is it possible to do that without using implicits?

This is by design. The ? means that a match is attempted with Any, and that means the match type does not reduce.

Isn’t that simply a Seq[T]?

1 Like

No, it can’t prove that Any (coming from your ?) is disjoint from 0. And it’s quite right not to be able to prove that, since they are not disjoint: 0 is both a subtype of Any and of 0.

1 Like

Ok, thanks, now I get it.

I just was hoping that it in that case it would be able to do a reverse matching, i.e. matching on the right-side types to detect a left-side type. I.e. that it would compare (String, String) first to EmptyTuple, then to Item *: EmptyTuple, then to Item *: Item *: EmptyTuple to determine whether Size is 0 or 1 or 2 (or, in case of the second declaration, first to EmptyTuple and then to Item *: <attempt reverse match recursively here>). And I really thought it was attempting to do it, but just something went bad. Now I see that I was assuming wrongly.

Off-topic on why Seq[T] is not what I need

Seq[T] can’t provide many compile-time guarantees that I require. Particularly, it can’t enforce (at compilation time) that some a and b (which are heterogeneous inside) have corresponding item types and sizes. For example, I want to enforce (at compilation time) that, when a is (Int, String, Boolean), then b is (MyExpr[Int], MyExpr[String], MyExpr[Boolean]) and c is (MyExpr[Iteratable[Int]], MyExpr[Iteratable[String]], MyExpr[Iteratable[Boolean]]).

Yes, that’s exactly Tuple.Map (it’s already heavily-used in my code). And, yes, I can use Tuple.IsMappedBy to check whether an externally-passed tuple has (F[?], F[?], …, F[?])-like “shape” (that’s what I need now). But that’s an implicit (similarly to the aforementioned Tuple.Union[T] <:< F[?] way); and today morning I had got a weird idea: why can’t I use specially-crafted type declarations instead of implicits in this case. So, thank you all, now I understand why.

It is interesting that EmptyTuple and Unit are different types

scala> val x:EmptyTuple = ()
-- [E007] Type Mismatch Error: -------------------------------------------------
1 |val x:EmptyTuple = ()
  |                   ^^
  |                   Found:    Unit
  |                   Required: EmptyTuple
  |
  | longer explanation available when compiling with `-explain`
1 error found

I thought about that. I think that’s unavoidable.

  • Unit de-facto acts almost as “yet another top type” in Scala, i.e. every type can be implicitly converted to Unit (although it gives a warning when the converted expression is just a val/var). That’s probably not very correct from theoretical point of view but currently works quite well in practice (because Unit is used almost never for parameters and extremely often as a result type).

  • EmptyTuple cannot take on this role, because EmptyTuple and especially its supertypes Tuple, Product, etc are quite often used as parameters. So making everything implicitly-convertible to EmptyTuple would introduce huge “leak” in type safety, for example, a function accepting EmptyTuple, say def f(a: EmptyTuple), would also accept any other value, including a scalar, a class instances or a non-empty tuple – by just silently ignoring them; the same would probably happen for a function accepting generic Tuple or Product, i.e. def f(a: Tuple) or def f(a: Product) (while an average user would rather expect auto-tupling to Tuple1 than implicit conversion to EmptyTuple).

So, EmptyTuple probably can’t be the same as Unit. (…Unless Scala will stop implicitly converting to Unit. But that would be too breaking change.)

3 Likes

a common idiom is

def sideEffecting(body: => Unit)

Thanks, but in the context of my comment to kjsingh (about why Unit can’t be easily made to be the same as EmptyTuple) that’s probably not very relevant (here it’s more like return type than like a parameter type).

Besides, there is one more obstacle: EmptyTuple is a subtype of AnyRef, while Unit isn’t.

1 Like

The original reason was that we needed EmptyTuple <: Product, and we could not make Unit <: Product without breaking Scala.js.

But your explanation is very interesting as well.

2 Likes