"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