scala3 icon indicating copy to clipboard operation
scala3 copied to clipboard

generic tuple not of subtype of ProductN

Open m8nmueller opened this issue 3 years ago • 10 comments

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.

m8nmueller avatar May 20 '22 22:05 m8nmueller

This is consistent with the current design as Tuple2[A, B] <:< (A, B) and not equal. Generic tuples are subtypes of Product.

nicolasstucki avatar May 23 '22 09:05 nicolasstucki

Maybe the mirrors could return MirroredElemTypes as Tuple2[A, B] instead of a (A, B) but this might have other unintended consequences.

nicolasstucki avatar May 23 '22 09:05 nicolasstucki

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?

m8nmueller avatar May 24 '22 12:05 m8nmueller

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?

nicolasstucki avatar May 24 '22 12:05 nicolasstucki

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.

Sounds promising :)

What does Encoder#forProductN do exactly?

You can encode any object as JSON by mapping it to a product (tuple) of objects which can be encoded by other means.

m8nmueller avatar May 24 '22 16:05 m8nmueller

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.

m8nmueller avatar May 24 '22 18:05 m8nmueller

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.

SethTisue avatar Sep 15 '22 16:09 SethTisue

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 Tuple2 docs starts at class (T1, T2), as though they're synonymous.

rossabaker avatar Apr 24 '23 18:04 rossabaker

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.

bplommer avatar May 02 '23 11:05 bplommer

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

matthughes avatar Apr 25 '24 01:04 matthughes