Case Class toString new behavior proposal (with implementation)

The PR https://github.com/scala/scala/pull/6972 has been merged adding the method productElementName to case classes. This means that it’s possible to implement the functionality propsed here without macros or runtime reflection

  def pretty(p: Product): String =
    p.productElementNames.zip(p.productIterator)
     .map { case (name, value) => s"$name=$value" }
     .mkString(p.productPrefix + "(", ", ", ")")

pretty(User("Susan", 42))
// User(name=Susan, age=42)

I started a discussion on adding such a pretty-printing function to the standard library https://github.com/scala/scala/pull/6972#issuecomment-411360323

7 Likes

and they lived happily ever after

:slight_smile: The end :slight_smile:

1 Like

I found myself coming back to this thread a few times to copy-paste the pretty function by @olafurpg. For anyone else doing similar, here is that function wrapped up in a trait for easier consumption

trait PrettyProduct extends Product {
  override def toString: String = {
    this
      .productElementNames
      .zip(this.productIterator)
      .map { case (name, value) => s"$name=$value" }
      .mkString(this.productPrefix + "(", ", ", ")")
  }
}
final case class Apple(a: Int, b: String) extends PrettyProduct

println(Apple(a=3, b="hello")); // Apple(a=3, b=hello)
7 Likes

Just stumbled on a draft PR by som-snytt that implements pretty printing of case classes with field names https://github.com/scala/scala/pull/8885

scala> val a = Sum("A", "B")
val a: Sum = Sum(exp = "A", exp2 = "B")

Personally I think this feature would greatly help with debugging and reading logs.

4 Likes

For me, in some cases it’s worthy, in others it’s too wordy :wink:
I would liked it better if the REPL just provided a Show type class that we can configure.

1 Like

Ammonites allows you to configure the pretty-printer, and with a bit of work could show field names too https://github.com/lihaoyi/PPrint/pull/38

1 Like

Is it thinkable, to change the behavior of generated toString to include both field names and properly quoted strings, at least in Scala 3, if it is too big of a change for Scala 2.x?

2 Likes

https://github.com/lihaoyi/PPrint gives you properly quoted strings, colors, nice indentation, streaming/incremental printing (e.g. for the first N lines of huge data structures) and a bunch of other things. If you’re still using println debugging, it’s definitely worth giving pprint.log a try

1 Like

Ideally with a way to easily create either a concise or verbose default implementation, as appropriate for the data structure.

PPrint 0.6.0 implemented support in Include field names in rendering of Product (Scala 2.13 only) #38, for example

case class Address(planet: String, city: String)
case class User(name: String, age: Int, address: Address)
pprint.pprintln(User("Picard", 75, Address("Earth", "San Francisco")), width = 80, height = 80)

outputs

User(
  name = "Picard",
  age = 75,
  address = Address(planet = "Earth", city = "San Francisco")
)
4 Likes

Yep! I was about the post here, but you beat me to it.

There’s a field you can override on PPrinter to revert to the old behavior, but the new behavior is useful enough i turned it on by default for Scala 2.13

5 Likes

Any chance that’ll get backported to 2.12?

From the PR:
“This is only available in Scala 2.13 because it makes use of productElementNames. The behaviour is unchanged in Scala 2.11/2.12.”

I’m aware, just wondering if that might change as this feature gets more traction

You mean adding the productElementNames method to case classes in 2.12?

From the outside that seems like it’d be the easiest way, depending on how complicated adding that would be.

Originally I was curious about @lihaoyi’s interest level in figuring how to get it into to PPrint for 2.12 - basically if the additional interest offset the work. I don’t really know what level of effort that would entail, so I figured I’d ask.

Binary compatibility forbids that.

Yeah forwards binary compatibility makes it rough to add that member to every case class.

But if you were willing to be JVM only, you could load and parse the case class’s classfile and look for debug symbols on the constructor to fish out the field names. Jackson-module-scala does this for JSON serialization, and I’ve put that in production so I know it works.

I don’t feel strongly enough about field names showing up in pprint on scala 2.12 to start parsing classfiles for debug symbols. But if anyone else does, feel free to send a PR! (though it would have to be an opt-in, since it wouldnt work on JS and native)

1 Like

BTW, if you don’t want to go fishing around bytecode yourself, you can use the Paranamer library https://github.com/paul-hammant/paranamer to do it for you. That’s what jackson-module-scala uses

Looks like with JDK8 you might not need paranamer at all https://stackoverflow.com/questions/21455403/how-to-get-method-parameter-names-in-java-8-using-reflection