"With" for records and the default constructor

David Alayachew davidalayachew at gmail.com
Wed Nov 27 01:20:27 UTC 2024


Hello Swaranga,

I think you are only looking at the general, most simplistic case, then
pointing to that as potential evidence of why withers are not as good of a
fit as nominal params.

The difference that withers bring to the table is that you can run loops,
statements, etc., in a wither. So, that gives you far more flexibility than
nominal parameters to create the values you need, and better yet, in a new
scope that keeps helper variables for only as long as needed.

With that in mind, Nominal params are solving a similar, but different
problem. They are simply allowing you to specify the exact variable name
that you are giving this value to, as opposed to using position.

It's sort of like the difference between an if statement and a ternary
operator. They serve similar goals, but their Venn Diagram of intents is
different. Or an if statement vs a switch statement. Again, overlap, but
not the same.

For more complex destructuring and restructuring, you would use a wither.

For far more simplistic variable assignment (especially in cases where
there isn't an old instance of the same type to extract values from), you
use named parameters.

Let me know if that is not clear.

On Tue, Nov 26, 2024 at 6:57 PM Swaranga Sarma <sarma.swaranga at gmail.com>
wrote:

> Brian, I totally get why withers are a bad way to introduce nominal
> parameters for the general case. But say in future, you do come up with a
> solution for nominal parameters, would then withers, instead, stick out
> like an unnatural/redundant feature in the language that exists only to
> clone objects? Say we have
>
> record Rec(int a, int b) {}
>
> var one = new Record(a: 10, b:42);
> var two = new Record(a: one.a(), b: 24)
>
> Nominal parameters, by design, seem to also address the problems solved by
> withers to a degree. Sure, I still have to specify every parameter in the
> nominal world and it is a little more verbose so they are not directly
> comparable, but it still seems very close. Would like to know how you are
> looking at it.
>
> Regards
> Swaranga
>
>
> On Tue, Nov 26, 2024 at 10:24 AM Brian Goetz <brian.goetz at oracle.com>
> wrote:
>
>> No, in part because we don't yet have a good enough solution, and in part
>> because there are other higher priorities at the moment.
>>
>>
>> On 11/26/2024 1:09 PM, Øyvind Kvien wrote:
>>
>> 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
>>> <https://urldefense.com/v3/__https://myservice/test__;!!ACWV5N9M2RV99hQ!ImZVGoRgncmogAFG0n7i4KRdOhw1kyEzqsIO-IXBkta5m2B1ycwHwb1WWaPmHcZtDQIdeFH1JKGiMmeEwA$>
>>> ";
>>>             }
>>>         };
>>>     }
>>>
>>> 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
>>> <https://urldefense.com/v3/__https://myservice/test__;!!ACWV5N9M2RV99hQ!ImZVGoRgncmogAFG0n7i4KRdOhw1kyEzqsIO-IXBkta5m2B1ycwHwb1WWaPmHcZtDQIdeFH1JKGiMmeEwA$>
>>> "
>>>         );
>>>
>>>         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/9d02bcf1/attachment-0001.htm>


More information about the amber-dev mailing list