"With" for records and the default constructor

Øyvind Kvien oyvind at kvien.no
Tue Nov 26 18:09:04 UTC 2024


Thank you for the answer. Yes nominal invocation and default parameter
values would be a better approach. Is there a JEP drafted for this feature
(I can't find one)?

On Tue, Nov 26, 2024 at 4:00 PM Brian Goetz <brian.goetz at oracle.com> wrote:

> 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/d44b4838/attachment-0001.htm>


More information about the amber-dev mailing list