Old style tuple accessors on normal case classes in Scala 3

The following code compiles on Scala 3.1.1 but not on Scala 2.13.8:

case class Text(number: Int, line: String)

object Main {
  val a = Text(42,"The answer") 
 
  def main(args: Array[String]): Unit = 
  { println(s"The number is '${a._1}'") 
    println(s"The line is '${a._2}'") } }

is this intentional or a bug? Btw, for normal classes it does not compile on Scala 3.1.1

Hmm. I am confused. You have an instance of Text referenced by a. The only way to access those values is via their names. As in a.number and a.line.

And as you said, this works in 3.1, but not in 2.13.

Here is the Scastie modifying your code to work in 2.13.

Here is the Scastie with exactly the same 2.13 modified code, but just set the compiler to 3.1, and it failed on val a = Text.unapply(text).get with an error message of value get is not a member of Playground.Text.

Unless something drastically changed in the transition to Scala 3.0, I don’t know why the code is failing.

Fascinating. The question, mind, is why this is working at all in Scala 3. I mean, there’s no reason why it should work, and I’m totally unsurprised that it fails in Scala 2. So this has to be a side-effect of some enhancement in Scala 3.

I would guess that it is coming out of the parameter untupling changes, but that’s just a guess based on two minutes of research, and I’m not at all sure that it’s plausible.

(My first guess had been that in Scala 3, your case class was now extending Product2 rather than just Product as in Scala 2; however, a quick experiment doesn’t seem to support that.)

1 Like

This is intentional. Case classes in Scala 3 define _N fields, combined with an unapply method that returns the case class instance itself this allows for pattern matching without unnecessary allocations (Scala 2 was also capable of matching on case classes without allocating, but this was done by special-casing case classes, whereas in Scala 3 it’s just a consequence of how they’re desugared)

4 Likes

Neat – I was wondering if it might be something of the sort. Is this enhancement spelled out anywhere in the Dotty docs? It’s a nice little detail, and seems to be pretty obscure currently…

1 Like

No, it doesn’t seem to be documented anywhere.

So, shouldn’t this be identified and documented in the “Transition from 2.x to 3.x Document” (cannot remember its official name) which explains all the differences?

And I find it fascinating there was no code in the huge testing corpus that didn’t use unapply. And if there is, then why didn’t those tests fail?!

I think this is where all the “transition from Scala 2 to Scala 3” notes are being collected and organized.

This was a helpful discussion, thanks for all the answers. I was suspecting it could be intentional, but was unsure, because i thought i read somewhere that the old style tuple accessors were later phased out. But now i assume they are not.

1 Like

I think we’re talking about two different things here – I’m referring to the new built-in ._1, ._2 feature, which isn’t documented yet. The rewrites to unapply were extensively documented, as @smarter linked to above.

As to the change: honestly, the pattern you show is kind of obscure – I didn’t even know it could be done that way in Scala 2. (I don’t recall seeing it documented anywhere.) And there are better ways to accomplish the same goal in Scala 3. So yes, it’s a language change, but seems like one of the more minor ones…

1 Like

I think it’s the other way around – this never worked for case classes before, but now it does. (That is, there is no “old style” about it.) Do you have reason to believe that tuple accessors worked here before Scala 3?

I am very happy to see the ._1 and ._2 feature was added. I hope it gets documented somewhere soon. I would have happily used the older pattern when transitioning my Scala 2 codebase, and been quite frustrated hitting this particular issue.

And I am honestly very glad to hear it is obscure. It wasn’t and isn’t for me and my codebase(s).

As such, it would still be nice to have this pattern “called out” as a migration issue guiding me to the meta and then the proper way to approach it in Scala 3.

These kinds of little edge cases can become a blocker when someone is trying to (quickly) move a codebase from Scala 2 to Scala 3.

Indeed, and this observation was exactly the reason I made de post. To find out which of the ones below holds true:

  1. Me being stupid
  2. New undocumented behaviour
  3. A bug in Scala 2: it should have always worked, but was never noticed.
  4. A bug in Scala 3: it should not work, but suddenly it does.

The second turns out to be the case.

Calling this “old style” is based on footnote 6 on page 83 of " Programming in Scala, Fifth Edition" stating “Note, prior to Scala 3, you accessed the elements of a tuple using one-based field names, such as _1 or _2.”

No, i did not. I knew it failed for Scala 2 so that is why i was surprised it suddenly did. (I noticed it while refactoring a tuple to a case class)

1 Like

… huh, that’s interesting phrasing, and I don’t think of it as having changed in normal cases. Is it referring to the fact that you can now do other things with tuples of more than 22 elements, or has something else changed?

I believe that note is in contrast to Scala 3’s 0-based integer indexing. For example:

scala> val t = ("Hello", 42, true)
val t: (String, Int, Boolean) = (Hello,42,true)

scala> t(0)
val res0: String = Hello

scala> t(1)
val res1: Int = 42

scala> t(2)
val res2: Boolean = true

scala> t(3)
-- Error: ----------------------------------------------------------------------
1 |t(3)
  | ^
  | Match type reduction failed since selector  EmptyTuple.type
  | matches none of the cases
  |
  |     case x *: xs => (0 : Int) match {
  |   case (0 : Int) => x
  |   case scala.compiletime.ops.int.S[n1] => scala.Tuple.Elem[xs, n1]
  | }
1 error found
2 Likes