Skip to content

Commit 222fac4

Browse files
olafurpgcb372
authored andcommitted
Revive productElementName to extract case class field names
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.
1 parent d79c006 commit 222fac4

File tree

1 file changed

+20
-0
lines changed

1 file changed

+20
-0
lines changed

library/src/scala/Product.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,24 @@ trait Product extends Any with Equals {
4848
* @return in the default implementation, the empty string
4949
*/
5050
def productPrefix = ""
51+
52+
/** The name of the n^th^ element of this product, 0-based.
53+
* In the default implementation, an empty string.
54+
*
55+
* @param n the index of the element name to return
56+
* @throws IndexOutOfBoundsException
57+
* @return the name of the specified element
58+
*/
59+
def productElementName(n: Int): String =
60+
if (n >= 0 && n < productArity) ""
61+
else throw new IndexOutOfBoundsException(n.toString)
62+
63+
/** An iterator over the names of all the elements of this product.
64+
*/
65+
def productElementNames: Iterator[String] = new scala.collection.AbstractIterator[String] {
66+
private[this] var c: Int = 0
67+
private[this] val cmax = productArity
68+
def hasNext = c < cmax
69+
def next() = { val result = productElementName(c); c += 1; result }
70+
}
5171
}

0 commit comments

Comments
 (0)