- Extension. The proposal outlines a notion of abstract record,
which provides a "width subtyped" hierarchy. Some have questioned
whether this carries its weight, especially given how Scala
doesn't support case-to-case extension (some see this as a bug,
others as an existence proof.) Records can implement interfaces.
I also suggest we avoid abstract records. A reference to one may seem
like a proper record but it will behave badly with regard to equals().
I don't see the upside compared to a common interface, and then you
don't have to have the novel parameterized extends clause.
The hackneyed example: consider the below as a subset of a typical
"model an expression with a tree" hierarchy. Of course, a real
hierarchy would have a lot more classes.
sealed interface Node;
record ValNode(int value) extends Node;
record VarNode(String name) extends Node;
abstract record BinOpNode(Node left, Node right) extends Node;
record PlusNode(Node left, Node right) extends BinOpNode(left, right);
record MulNode(Node left, Node right) extends BinOpNode(left, right);
Obviously there might be some common behavior for binary operation nodes
that can be factored up into BinOpNode. But also, there are times when
matching against the abstract type makes sense too. For example, if you
want to traverse the tree and perform structural operations (say, detect
if a tree contains a reference to the variable "x"), matching on
abstract records is pretty useful:
boolean containsVar(Node node, String name) {
return switch (node) {
case VarNode(String s) -> s.equals(name);
case BinOpNode(var left, var right) -> containsVar(left,
name) || containsVar(right, name);
default -> false;
}
}
A client who is only interested in structural properties can match once
on the abstract type, instead of matching explicitly on N effectively
identical cases (and add more every time the hierarchy changes.)
On the other hand. As much as I want everyone to stick to immutable
records as much as possible, it seems very costly to me to have to
introduce a new keyword for "not final", and have users keep track of
which things have which defaults. Let this just be "best practice",
like it already is for regular fields (make them final unless you have
good reason not to).
Pretend we already had non-final. Does that change your inclination?
(When we do sealed types, we're likely going to need a way to say
non-sealed anyway.)
- Accessors. Perhaps the most controversial aspect is that
records are inherently transparent to read; if something wants to
truly encapsulate state, it's not a record. Records will
eventually have pattern deconstructors, which will expose their
state, so we should go out of the gate with the equivalent. The
obvious choice is to expose read accessors automatically. (These
will not be named getXxx; we are not burning the ill-advised
Javabean naming conventions into the language, no matter how much
people think it already is.) The obvious naming choice for these
accessors is fieldName(). No provision for write accessors;
that's bring-your-own.
Method and field named identically is a slight concern. If we gain
field references using the same syntax as method references there
would probably be no way to refer to such a field. I'm pretty sure
this is not worth worrying about though.
I have a story for disambiguating field references...
Records could be safely made cloneable() with automatic support
too (like arrays), but not clear if this is worth it (its darn
useful for arrays, though.)
People just really need to not use arrays anymore, and especially not
with records. imho we should have added immutable List and
ImmutableIntArray etc. classes a very long time ago. I know we won't
now due to our value type aspirations. In the meantime we're in a
weird place. Arrays are completely terrible except as
micro-optimizations to be used with great care.
OK, but do you have an opinion on whether records should automatically
acquire a clone() implementation?