Pre-SIP: Improved support for Java records

See here for the current (draft) proposal. It is still incomplete, but I think it is in a fleshed-out enough state to start the pre-sip discussion.

There are several outstanding questions namely about mirror support which I’m still unclear on:

  1. Should productArity and productElement be promoted onto the Mirror trait?
  2. Should a new anonymous class be created each time at the callsite; does the reasoning which went into including the TupleMirror apply here?
  3. Is it a problem to include a class which only compiles with JDK16+ in the compilation process of the runtime library?
  4. What happens if I refer to a method on the companion object for the Java class at runtime? Do we still go through the same desugar steps in the compiler with a compiled jar?

I have also left the specification until some of the questions are (hopefully) answered by the Pre-SIP discussion.


It should be noted, that the current eco-system implicitly assumes that a Mirror.Product returns a class which extends Product even though there are no type bounds on the actual trait itself. This is implied by the example in the official documentation and can be seen in the source code of well-used libraries in the wild.
Releasing the changes proposed would mean that a codec would be able to be derived for a record, but the program would fail at runtime — which is not ideal behaviour.

This is pretty unfortunate indeed. One way to work around this would be to create new types that would generalize the existing Mirror types. This might be worth it if we’d like to support other non-Product things, for example, classes with multiple parameter lists. It could also let us replace fromProduct by something that takes into account apply methods: Case class deserialization doesn't go through `apply` in Scala 3 · Issue #552 · com-lihaoyi/upickle · GitHub

1 Like

It’s going to make our build a bit painful but it shouldn’t be a showstopper.

Something equivalent to your desugaring logic will need to be added to dotty/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala at main · lampepfl/dotty · GitHub probably at the same place where we currently call NamerOps.addConstructorProxies which is another kind of magic inline method.

I like this idea of adding new mirrors,

One other relevant suggestion is here from @lihaoyi. If we introduced a new mirror type which was something like,

trait Mirror.NamedProduct:

  // type aliases same as `Mirror.Product`
  type MirrorElemLabels <: Tuple
  type MirroredLabel <: String
  type MirrorMonoType

  // These allow a user of the mirror to abstract over named products at runtime
  def productArity: Int
  def productElementAt(i: Int): Any

  def fromProduct(p: Product): MirroredMonoType

And make it derivable for both T <: NamedTuple and records.

Perhaps we could even use it to address @odersky point from below:

But the problem is we can’t abstract over it. If we want to have a generic type that reflects names and types, we are back to NamedTuple.

Since you can use the methods on the mirror to abstract over the T <: NamedProduct.