-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Add productElementName and productElementNames methods to Product #6972
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Oops, just noticed that the behaviour is also incorrect for multiple param lists. The synthetic only picks up the first param list. Investigating now. |
Scratch that. I misunderstood the behaviour of
|
// def productElementNameMethod = perElementMethod(nme.productElementName, StringTpe)(x => LIT(x.name.toString)) | ||
def productElementNameMethod = { | ||
val constrParamAccessors = clazz.constrParamAccessors | ||
createSwitchMethod(nme.productElementName, constrParamAccessors.indices, StringTpe)(idx => LIT(constrParamAccessors(idx).name.dropLocal.toString)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably should be .name.dropLocal.decodedName
to fix:
scala> case class Symbols(:: : Int, || : Int)
defined class Symbols
scala> Symbols(0, 0).productElementNames.toList
res4: List[String] = List($colon$colon, $bar$bar)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Added a test case for this.
I did a quick empirical measurement of the classfile size impact for a simple case class. case class FiveFields(a: Int, b: Int, c: Int, d: Int, e: Int) Scala 2.12.6:
This PR:
Regarding @retronym's comment from the previous PR:
I tried that but it actually makes the classfile slightly bigger. It reduces the bytecode size for the This PR as it stands:
Using a factory method to build the exception:
The file size increases from 6291 to 6324 bytes. The factory method looks like this: package scala
package object runtime {
@noinline
final def aioobe(n: Int): ArrayIndexOutOfBoundsException =
new ArrayIndexOutOfBoundsException(n.toString)
} and the callsite looks like this: def createSwitchMethod(name: Name, range: Seq[Int], returnType: Type)(f: Int => Tree) = {
createMethod(name, List(IntTpe), returnType) { m =>
val arg0 = Ident(m.firstParam)
val default = DEFAULT ==> Throw(Apply(getMemberMethod(RuntimePackage, TermName("aioobe")), arg0))
val cases = range.map(num => CASE(LIT(num)) ==> f(num)).toList :+ default
Match(arg0, cases)
}
} |
A call to a real Java static method is a bit smaller in bytecode that a module load and a virtual call. |
025ff7f
to
e5c2d2f
Compare
I think this one is now ready to go, unless anyone has any more comments. I'd like to investigate the static factory method optimisation further and potentially submit a separate PR for it later, if that's OK. I don't think it needs to block this PR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Thank you Chris and Olaf.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m not convinced by this feature. In addition to the (small, I concede) impact on the bytecode size, introducing this feature means that we will have to support it afterwards. But is it really worth it? It encourages using a poorly typed API and we already have better tools for generic programming (e.g. shapeless or scalaz-deriving).
I thought the main purpose of product element name is to define some nonstandard formatters for data classes. The standard toString for case classes and tuples doesn't for example quote strings, which makes converting ScalaTest output (i.e. Using scalaz-deriving or shapeless is going to not only increase classpath size considerably and possibly introduce JAR hell for such a small feature, but they require some changes in the code. This PR adds method which could be used by e.g. ScalaTest out of the box without any changes to user code. |
not to mention the impact on compile times... personally I'm convinced this should go in. @tarsa's use case simply shouldn't be hard, this is really basic functionality, lacking this just makes Scala look silly and bad. sure, yes, if people need to be warned of limitations of more principled, highfalutin' solutions will remain available for those who want them. @lrytz what do you think? needs rebase. |
e5c2d2f
to
0dd90c1
Compare
Rebased. |
LGTM too. @cb372 could you sqash all changes into a single commit? |
0dd90c1
to
6801525
Compare
Squashed (and rebased again) |
The build now fails with a compile error, but I think it's unrelated to my changes. I see the same error on 2.13.x branch: https://travis-ci.org/scala/scala/jobs/413124970#L1021 |
Thanks, I'll look at the build failure. |
#7010 should fix the build, you can rebase again when it's merged. |
This commit adds two methods to the `scala.Product` trait: ```scala trait Product { /** Returns the field name of element at index n */ def productElementName(n: Int): String /** Returns field names of this product. Must have same length as productIterator */ def productElementNames: Iterator[String] } ``` Both methods have a default implementation which returns the empty string for all field names. This commit then changes the code-generation for case classes to synthesize a `productElementName` method with actual class field names. The benefit of this change is that it becomes possible to pretty-print case classes with field names, for example ```scala case class User(name: String, age: Int) def toPrettyString(p: Product): String = p.productElementNames.zip(p.productIterator) .map { case (name, value) => s"$name=$value" } .mkString(p.productPrefix + "(", ", ", ")") toPrettyString(User("Susan", 42)) // res0: String = User(name=Susan, age=42) ``` The downside of this change is that it produces more bytecode for each case-class definition. Running `:javacp -c` for a case class with three fields yields the following results ```scala > case class A(a: Int, b: Int, c: Int) > :javap -c A public java.lang.String productElementName(int); Code: 0: iload_1 1: istore_2 2: iload_2 3: tableswitch { // 0 to 2 0: 28 1: 33 2: 38 default: 43 } 28: ldc 78 // String a 30: goto 58 33: ldc 79 // String b 35: goto 58 38: ldc 80 // String c 40: goto 58 43: new 67 // class java/lang/IndexOutOfBoundsException 46: dup 47: iload_1 48: invokestatic 65 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer; 51: invokevirtual 70 // Method java/lang/Object.toString:()Ljava/lang/String; 54: invokespecial 73 // Method java/lang/IndexOutOfBoundsException."<init>":(Ljava/lang/String;)V 57: athrow 58: areturn ``` Thanks to Adriaan's help, the estimated cost per `productElementName` appears to be fixed 56 bytes and then 10 bytes for each field with the following breakdown: * 3 bytes for the [string info](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4.3) (the actual characters are already in the constant pool) * 4 bytes for the tableswitch entry * 2 bytes for the ldc to load the string * 1 byte for areturn In my opinion, the bytecode cost is acceptably low thanks to the fact that field name literals are already available in the constant pool.
6801525
to
efc4821
Compare
What is the use case for this? As @SethTisue mentioned people ask about this constantly on Gitter and so on, but in the end it's never what they really want. |
what @tarsa said. but also other tasks involving abstracting over multiple case classes. yes, doing that with
I disagree. Certainly some of them eventually change their minds about what they want, later in their Scala journeys. I'll admit there is an irreducible element of speculation here. What happens next, after their question on Gitter/SO/etc? We don't have a team of people in white lab coats with clipboards tracking these people afterwards. But my best guess is that 1) a substantial portion eventually decide they need a "proper" solution like shapeless, but also 2) a substantial portion are able to get their job done with |
The two most obvious use cases that spring to mind are:
For the former, I think it's a no-brainer that you should be able to do that without having to reach for an external library, especially a "heavyweight" library like shapeless, scalaz-deriving or magnolia, or roll it yourself using macros/scalameta. It should just work out of the box. (It's too late to change the format of the As for the latter, that's something I've had to do a reasonable number of times in the past. It would be convenient to be able to achieve it with a few lines of code and no dependencies, instead of depending on shapeless or whatever. |
|
com-lihaoyi/PPrint#4 is an issue in the wild where this could be used |
This is green and yours to land, @lrytz (already 👍 from me). |
@tpolecat pretty-printing libraries like http://www.lihaoyi.com/PPrint/ already handle collection types so Note that |
Hurray! Thank you for everybody for the reviews and discussions 🎉 I'm wondering if Judging by the discussions in https://contributors.scala-lang.org/t/case-class-tostring-new-behavior-proposal-with-implementation/2056/43 there is a big demand to pretty-print case classes that can be copy-pasted back into source code and compile. Some requirements
(essentially, I would love to have pprint in the standard library 😄 ) Where would be the best place to discuss this further? Contributors, scala/scala-dev, scala/bug? |
yes please https://contributors.scala-lang.org |
Noting for posterity: This change caused a slight slowdown (700ms to 715ms) in the The simplest explanation is that that corpus includes a higher ratio of case classes to total LoC that the others, and the cost of synthesizing, transforming, and code-gen for the new method is visible. I haven't dug deeper to confirm this though. |
Why does List extend Product? |
let's discuss on https://contributors.scala-lang.org rather than take this PR off on a tangent |
I asked because the answer could be relevant to some of the challenges here. I don't think it's a tangent but it's probably moot since it's merged and late. |
Are we continuing on the old thread contributors.scala-lang.org? |
aaaand we have our first user! #7242 |
@olafurpg Did this discussion (Product.productToString) happen somewhere else ? Cannot seem to find it. It would be great if you could point me to it |
@MariosPapasofokli the last discussion on the topic was around here https://contributors.scala-lang.org/t/case-class-tostring-new-behavior-proposal-with-implementation/2056/44?u=olafurpg |
@olafurpg thank you |
This continues @olafurpg's work in #6951.
Starting with his commit, I've:
productElementName
to use the (decoded) case class parameter names instead of the accessor names