"With" for records and the default constructor

Brian Goetz brian.goetz at oracle.com
Tue Nov 26 15:00:36 UTC 2024


This has been well covered in several previous mails.

The reason this request has not seen favorable reception is because it 
is actually a request for a _different_ feature -- just with a worse 
syntax.  What you really want is nominal (rather than positional) 
invocation of constructors (like, for example, `new Foo(a: 1, b: 2)`).  
And we totally get that people really, really want this.  We're just not 
going to cram a bad version of it into the language because people want 
it so badly.

Why this would be a bad version of nominal invocation is evident in your 
mail: you talk about using withers with "default" constructors, but 
records don't have default constructors.  And you point out why: that 
often there is no good default value for record components.  So in the 
solution you are suggestion, there is no "withing" at all; it is not 
deriving one record from another.  It is just trying to attach an 
unrelated feature to the wither proposal, because it kinda sorta looks 
like it (and it feels like the train is leaving the station and if we 
don't throw this extra feature on it, we might never get it!)  This is 
not the way to evolve the language.

Compared to nominal invocation, the `with` version is:

  - More confusing, because there is no withing going on
  - More verbose
  - Offers no path to nominal invocation for instance members (e.g., 
`x.foo(a: 1)`)

So it is a much worse version of "nominal invocation."  Its the sort of 
thing people would be happy about for about five minutes, but would 
likely be unhappy about thereafter.

And, when we say what people really want is nominal invocation, what we 
mean is that they really want nominal invocation _with defaultable 
parameters_.  Because this is what gets rid of the builders.

So yes, we deeply get that there is a pain point here.  But this is not 
the solution.










On 11/26/2024 5:13 AM, Øyvind Kvien wrote:
> I see there're no plans of including the default constructor with 
> withers for record creation, and a manually created static builder has 
> previously been suggested instead with default values in the 
> constructor. In my opinion ommiting the default constructor to be used 
> with withers would be a big mistake.
>
> A very common scenario in smaller services is to use simple data 
> structures to pass data to the api of other services and message 
> brokers. In these cases the data contains mostly primitive types and 
> (these are not domain models). Setting default values manually in the 
> default constructor, as suggested would be very tedious and lots of 
> typing. Also many times a default value is not even possible since it 
> could mean setting the object in a wrong initial state. Forcing the 
> use of default values I think is a bad approach.
>
> It would therefore be immensely useful if the default record 
> constructor could be used with withers. The compiler needs of course 
> to enforce that all parameters in the default constructor are set.
>
> Below is a real world example of how building a request body to an 
> internal service in my organization, used for getting access tokens 
> for test purposes, would look like using withers and the default 
> constructor. The following records describes the simple data structure 
> for the request body to the api of the service (which gets serialized 
> to json).
>
>     record RequestBody(
>             String audience,
>             boolean withoutDefaultClientClaims,
>             boolean withoutDefaultUserClaims,
>             boolean createDPoPTokenWithDPoPProof,
>             ExpirationParameters expirationParameters,
>             ClientClaimsParameters clientClaimsParameters,
>             @Nullable UserClaimsParameters userClaimsParameters,
>             @Nullable DPoPProofParameters dPoPProofParameters) {
>     }
>
>     record ExpirationParameters(
>             int expirationTimeInSeconds) {
>     }
>
>     record ClientClaimsParameters(
>             List<String> scope,
>             String orgnrParent,
>             String clientId,
>             String clientName,
>             String jti) {
>     }
>
>     record UserClaimsParameters(
>             String pid,
>             String hprNumber,
>             String name,
>             String givenName,
>             String middleName,
>             String familyName,
>             String securityLevel,
>             String assuranceLevel,
>             String amr) {
>     }
>
>     record DPoPProofParameters(
>             String htuClaimValue,
>             String htmClaimValue) {
>     }
>
> Using withers with the default constructor would look as suggested in 
> the method below. It's very easy to visualise the json structure that 
> the RequestBody record get serialized into when the parameter names 
> are present.
>
> Also there's no need to use default values which is a big win, and the 
> compiler enforces that all parameters are set. This way the record is 
> never instantiated in a wrong state.
>
> Note that no parenthesis are used after 'new RequestBody' for the 
> default constructor as a syntax suggestion.
>
>     public static RequestBody createRequestBodyUserToken(
>             @Nullable String clientId,
>             @Nullable String userNin
>     ) {
>         return new RequestBody with { // No parenthesis after 'new 
> RequestBody' for the default constructor.
>             audience = "audience";
>             withoutDefaultClientClaims = true;
>             withoutDefaultUserClaims = true;
>             createDPoPTokenWithDPoPProof = true;
>             expirationParameters = new ExpirationParameters with {
>                 expirationTimeInSeconds = 300;
>             };
>             clientClaimsParameters = new ClientClaimsParameters with {
>                 scope = List.of("openid", "scope");
>                 orgnrParent = "12345";
>                 clientId = clientId != null ? clientId : 
> "13edc8d1-3fa2-425a-9b53-c346df79e589";
>                 clientName = "SmokeTest";
>                 jti = UUID.randomUUID().toString();
>             };
>             userClaimsParameters = new UserClaimsParameters with {
>                 pid = userNin != null ? userNin : "12345678912";
>                 hprNumber = "987654";
>                 name = "Half Badger";
>                 givenName = "Half";
>                 middleName = "";
>                 familyName = "Badger";
>                 securityLevel = "4";
>                 assuranceLevel = "high";
>                 amr = "pwd";
>             };
>              dPoPProofParameters = new DPoPProofParameters with {
>                 htuClaimValue = "GET";
>                 htmClaimValue = "https://myservice/test";
>             }
>         };
>     }
>
> The alternative, as of today, is to first instantiate each record 
> separately and then lastly instantiate the RequestBody and return it. 
> It's harder to read as the parameter names get lost.
>
>     private static RequestBody createRequestBodyUserToken(
>             @Nullable String clientId,
>             @Nullable String userNin
>     ) {
>         var expirationParameters = new ExpirationParameters(
>              300
>         );
>
>         var clientClaimsParameters = new ClientClaimsParameters(
>                 List.of("openid","scope"),
>                 "12345",
>                 clientId != null ? clientId : 
> "13edc8d1-3fa2-425a-9b53-c346df79e589",
>                 "SmokeTest",
>                 UUID.randomUUID().toString()
>         );
>
>         var userClaimsParameters = new UserClaimsParameters(
>                 userNin != null ? userNin : "12345678912",
>                 "987654",
>                 "Half Badger",
>                 "Half",
>                 "",
>                 "Badger",
>                 "4",
>                 "high",
>                 "pwd"
>         );
>
>         var dPoPProofParameters = new DPoPProofParameters(
>                 "GET",
>                 "https://myservice/test"
>         );
>
>         return new RequestBody(
>                "audience",
>                 true,
>                 true,
>                 true,
>                 expirationParameters,
>                 clientClaimsParameters,
>                 userClaimsParameters,
>                 dPoPProofParameters
>         );
>     }
>
> I therefore hope that the default record constructor can be included 
> with withers! It would be very useful when working with any type of api.
>
> Regards
> Øyvind Kvien
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20241126/1797ca84/attachment-0001.htm>


More information about the amber-dev mailing list