Make Primary Constructor an independant feature

Brian Goetz brian.goetz at
Fri Apr 12 19:52:59 UTC 2019

> I think there is a merit to separate the primary constructor feature from other features of a record.

You could be saying one of two things here:

  - Let's streamline generation of constructors, as record constructors 
do, by allowing some way of indicating that parameters are bound to 
fields, that all classes can use.
  - Let's define a different top-level streamlined idiom, `class 

For the first, I agree, and already have most of a design story for 
this, but am deliberately holding it back because we are already deeply 
in danger of drowning in "how about this (mostly-syntax-oriented) 
feature" and not ever delivering anything. In fact, if you go back to my 
early talks about Amber several years ago, you'll note that I said 
"Records are almost surely a 'macro' for a set of finer-grained features 
that can be applied more generally."  Of the various pieces, 
"field-bound constructors" are the simplest, and they carry over almost 
directly from records.

As to the second: this is a bottomless pit of worms (I went down that 
route, didn't like it.)  Various problems:
  - The fields don't belong up in the header, if they are not part of 
the classes API.
  - Once you admit mutable state, you quickly get mired in "but I want 
setters for these fields", "but I want my getters to be protected", "but 
I want only these fields to participate in equals/hashCode", etc.

What you've basically said here is "Let's throw records in the garbage 
and design something new, more like "case classes"."  (I know you 
probably didn't mean to say that, but that's really what you're 

Records work because all the protocols (representation, construction, 
deconstruction, equals, hashCode, toString) are all derived from the 
_same_ description.  Once once departs, the chances that another wants 
to depart is very high.  At which point, you either end up at a macro 
generator with too many knobs (more than zero is probably too many), or 
you end up with an assortment of lower-level features like "make me a 
constructor", "make me an equals/hashCode", etc.

Records also work because they have clear semantics.  Once you start to 
get into macro-generation land, syntax takes over, and you lose the 
guiding light of semantics.

I think the latter path -- let's find the low-level features that 
records are really a bundle of -- is a much more sound one.  And I'm 
willing to walk it, but not right now.

> This afternoon while fixing a bug, i took a look to the classes around to see if it was possible to transform them to records. But i've found that in more than half of the cases, the classes were not representing data but just carrier so having the getters, hashCode and equals automatically generated was the factor that hamper me to transform the classes into records.
> Given that a record is a data carrier, i'm wondering if it makes sense to say that a record = data + carrier i.e. to separate the data part (getters + equals + hashCode) from the carrier part (the initialization using a primary constructor). The idea is that a primary constructor can be applied not only to records, but also to classes, enums and later value classes.
> Here are some examples,
>    For a result of a function, having an equals/hashCode can be harmful (maybe the error code is not stable)
>      class Result(int value, int errorCode);
>    For most of the structural patterns, having getters is not helpful
>      class UserProxy(User user) implements User {
>        UserProxy {
>          requireNonNull(user);
>        }
>        ...
>      }
>    An enum that stores an integer, all constants have to initialize the value
>      enum Flag(int flag) {
>        READ(8), WRITE(16)
>        ;
>        public boolean isAllowedBy(int flags) {
>          return flags & flag != 0;
>        }
>      }
> Rémi

More information about the amber-spec-experts mailing list