I completely agree that named <:< unnamed
is a bad subtyping relationship, for the reasons stated.
But I’m not sold on unnamed <:< named
either. Method names are in covariant position, which means that adding them should make you a subtype. That’s how it always works with everything else, and it sure feels like named tuples are “unnamed tuples but with method names added”. It’s really hard to shake that impression.
// How can you avoid thinking this??
class (foo: Foo, bar: Bar) extends (Foo, Bar):
def foo = _1
def bar = _2
// Especially given this:
// scala> ("eel", "cod").getClass.getMethods.find(_.getName == "_1")
// val res11: Option[java.lang.reflect.Method] =
// Some(public java.lang.Object scala.Tuple2._1())
Furthermore, unnamed <:< named
is useful in some situations but not in others. For example, if the entire point of (start: Int, length: Int)
vs (start: Int, end: Int)
is to make sure you interpret the pair of integers correctly, do we really just want to blithely pass in (3, 7)
?!
And what is the interaction with structural typing?
def bippy(withFoo: { def foo: Foo }) = ???
val nt = (foo = Foo(), why = "example")
bippy(nt) // Does this work? If not, why not?
val ut = (Foo(), "example")
bippy(ut) // We don't seriously want this to work, do we?!
This suggests to me that as tempting as subtyping might be to get the methods on named tuples to support names, we probably don’t want unnamed tuples as subtypes of named tuples.
Instead, I think the best analogy from within the language as it stands now is that named tuples are automatically generated opaque types over regular tuples.
opaque type Tuple2_foo_Foo_bar_Bar = (Foo, Bar)
object Tuple2_foo_Foo_bar_Bar {
inline def fromTuple(fb: (Foo, Bar)): Tuple2_foo_Foo_bar_Bar = fb
extension (named: Tuple2_foo_Foo_bar_Bar)
inline def toTuple: (Foo, Bar) = named
inline def foo: Foo = named._1
inline def bar: Bar = named._2
}
plus compiler support for matching and so on. This also has downsides–you won’t get the names printed, for instance–but I think it’s the best straightforward compromise, and it has the advantage that you don’t need to unpack and repack objects just to get the used names straight.
Anyway, I’m not sure about this as a solution, but I’m pretty convinced by your arguments that named tuples as subtypes of unnamed tuples isn’t really what we want, but I am also convinced by other arguments that unnamed tuples as subtypes of named tuples also has substantial problems.
In the absence of a way around one or the other, the only solution is to not have a subtyping relationship.