"With" for records and the default constructor
Swaranga Sarma
sarma.swaranga at gmail.com
Wed Nov 27 20:05:37 UTC 2024
Thank you. This makes sense now.
Regards
Swaranga
On Tue, Nov 26, 2024 at 5:21 PM David Alayachew <davidalayachew at gmail.com>
wrote:
> 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/20241127/91d39b09/attachment-0001.htm>
More information about the amber-dev
mailing list