scala3
scala3 copied to clipboard
generic tuple not of subtype of ProductN
Compiler version
3.1.2
Minimized code
case class Foo(a: Int, b: String)
val foo = Foo(1, "Hello")
var x: Tuple2[Int, String] = Tuple.fromProductTyped(foo)
var y: Product2[Int, String] = x
var z: Product2[Int, String] = Tuple.fromProductTyped(foo)
Output
-- [E007] Type Mismatch Error: -------------------------------------------------
5 |var z: Product2[Int, String] = Tuple.fromProductTyped(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
| Found: (Int, String)
| Required: Product2[Int, String]
Explanation
===========
Tree: Tuple.fromProductTyped[Foo](foo)(
Foo.$asInstanceOf[
(
deriving.Mirror.Product{
MirroredType = Foo; MirroredMonoType = Foo; MirroredElemTypes <: Tuple
}
&
scala.deriving.Mirror.Product{
MirroredMonoType = Foo; MirroredType = Foo;
MirroredLabel = ("Foo" : String)
}
){
MirroredElemTypes = (Int, String);
MirroredElemLabels = (("a" : String), ("b" : String))
}
]
)
I tried to show that
(Int, String)
conforms to
Product2[Int, String]
but the comparison trace ended with `false`:
==> (Int, String) <: Product2[Int, String]
<== (Int, String) <: Product2[Int, String] = false
The tests were made under the empty constraint
Expectation
As (A, B) = Tuple2[A, B] is a direct subclass of Product2[Int, String], I would expect that the assignment to z compiles.
This is consistent with the current design as Tuple2[A, B] <:< (A, B) and not equal. Generic tuples are subtypes of Product.
Maybe the mirrors could return MirroredElemTypes as Tuple2[A, B] instead of a (A, B) but this might have other unintended consequences.
I don't know the compiler good enough to anticipate the results, but I think it is rather useful feature in absence of the traditional "unapply".
My use case are circe's Encoder#forProductN, which require a function from (case class) instance to ProductN. Are there other possibilities to implement that trivially or should that interface be changed?
One of the long terms goals of generic tuples is to make the TupleN encoding redundant and at some point remove it and only keep one kind of tuple. This also implies that ProductN would not be part of tuples.
My use case are circe's Encoder#forProductN, which require a function from (case class) instance to ProductN. Are there other possibilities to implement that trivially or should that interface be changed?
It seems that you need to use mirrors to implement that. What does Encoder#forProductN do exactly?
One of the long terms goals of generic tuples is to make the
TupleNencoding redundant and at some point remove it and only keep one kind of tuple.
Sounds promising :)
What does
Encoder#forProductNdo exactly?
You can encode any object as JSON by mapping it to a product (tuple) of objects which can be encoded by other means.
This is consistent with the current design as
Tuple2[A, B] <:< (A, B)and not equal.
But why does the assignment to x work, then? If fromProductTyped returns (Int, String) which is not <:< Tuple2[…], the variable type should mismatch.
It's really.... shall we say, disquieting that the subtyping here isn't transitive:
scala> summon[Int *: Int *: EmptyTuple <:< Tuple2[Int, Int]]
val res11: (Int, Int) =:= (Int, Int) = generalized constraint
scala> summon[Tuple2[Int, Int] <:< Product2[Int, Int]]
val res12: (Int, Int) =:= (Int, Int) = generalized constraint
scala> summon[Int *: Int *: EmptyTuple <:< Product2[Int, Int]]
-- Error: ----------------------------------------------------------------------
1 |summon[Int *: Int *: EmptyTuple <:< Product2[Int, Int]]
| ^
| Cannot prove that (Int, Int) <:< Product2[Int, Int].
1 error found
🙀
It's rather cold comfort to know that Tuple2 might eventually go away in some possible future.
This is consistent with the current design as Tuple2[A, B] <:< (A, B) and not equal.
Is that the future design or the current design? They seem currently equal to me:
-
There is a witness they're equal:
summon[(Int, Int) =:= Tuple2[Int, Int]] -
The graph on
Tuple2docs starts atclass (T1, T2), as though they're synonymous.
It's really.... shall we say, disquieting that the subtyping here isn't transitive:
Agreed - I'm struggling to see how this issue is an “enhancement” rather than a bug.
Fixing this could make it a lot easier to move away from derivation. This seems like it should compile:
case class Foo(x: Int, y: String)
val t: Tuple2[Int, String] = Tuple.fromProductTyped(Foo(1, "a"))
val p1: Product2[Int, String] = p1
// fails to compile: Found (Int, String); required Product2[Int, String]
val p2: Product2[Int, String] = Tuple.fromProductTyped(Foo(1, "a"))