Records: construction and validation

Remi Forax forax at univ-mlv.fr
Mon Mar 12 22:31:45 UTC 2018


Thinking only in term of constructor is only half of the story of preconditions,
you also need precondition on setters (and getters for the defensive copy), that's the advantage of moving the requirements in its own 'section', you can distribute the code to the different generated part.

By example, if requires say that x is positive
  record B(int x) 
    requires x >= 0;

it means that the constructor and the x() throw an exception if the parameter x is not positive.

Rémi

BTW normalizing arguments is nice way to lost the trust of your users, the behavior is obvious if you are the writer of the class and it's a real pain if you are the poor user that had to figure out why when he send a value, the code acts as if it is another value. So normalization should always occur at call site and never at callee site. And it's the same issue with default values.

----- Mail original -----
> De: "Brian Goetz" <brian.goetz at oracle.com>
> À: "amber-spec-experts" <amber-spec-experts at openjdk.java.net>
> Envoyé: Lundi 12 Mars 2018 18:48:35
> Objet: Records: construction and validation

> Here's a sketch of where our thinking is right now for construction and
> validation.
> 
> General goal: As Kevin pointed out, we should make adding incremental
> validation easy, otherwise people won't do it, and the result is worse
> code.  It should be simple to add validation (and possibly also
> normalization) logic to constructors without falling off the syntactic
> cliff, either in the declaration or the body of the constructor.
> 
> All records have a /default constructor/.  This is the one whose
> signature matches the class signature.  If you don't have an explicit
> one, you get an implicit one, regardless of whether or not there are
> other constructors.
> 
> If you have records:
> 
>     abstract record A(int a) { }
>     record B(int a, int b) extends A(a) { }
> 
> then the behavior of the default constructor for B is:
> 
>     super(a);
>     this.b = b;
> 
> If you want to provide an explicit constructor to ensure, for example,
> that b > 0, you could just say it yourself:
> 
>     public B(int a, int b) {
>         if (b <= 0)
>             throw new IllegalArgumentException("b");
>         super(a);
>         this.b = b;
>     }
> 
> Wait, wait a second...  I thought we couldn't put statements ahead of
> the super-call?
> 
> DIGRESSION...
> 
> Historically, this() or super() must be first in a constructor. This
> restriction was never popular, and perceived as arbitrary. There were a
> number of subtle reasons, including the verification of invokespecial,
> that contributed to this restriction.  Over the years, we've addressed
> these at the VM level, to the point where it becomes practical to
> consider lifting this restriction, not just for records, but for all
> constructors.
> 
> Currently a constructor follows a production like:
> 
>     [ explicit-ctor-invocation ] statement*
> 
> We can extend this to be:
> 
>     statement* [ explicit-ctor-invocation statement* ]
> 
> and treat `this` as DU in the statements in the first block.
> 
> ...END DIGRESSION
> 
> OK, so we can put a statement ahead of the super-call.  But this
> explicit declaration is awfully verbose.  We can trim this by:
>  - Allow the compiler to infer the signature for the default
> constructor, if none is provided;
>  - Provide a shorthand for "just do the default initialization".
> 
> Now we get:
> 
>     public B {
>         if (b <= 0)
>             throw new IllegalArgumentException("b");
>         default.this(a, b);
>     }
> 
> There's still some repetition here; it would be nice if the default
> initialization were inferred as well.  Which leads to a question: if we
> have a record constructor with no explicit constructor call, do we do
> the default initialization at the beginning or the end?  In other words,
> does this:
> 
>     public B {
>         if (b <= 0)
>             throw new IllegalArgumentException("b");
>     }
> 
> mean
> 
>     public B {
>         if (b <= 0)
>             throw new IllegalArgumentException("b");
>         default.this(a, b);
>     }
> 
> or this:
> 
> public B {
> default.this(a, b);
> if (b <= 0)
>             throw new IllegalArgumentException("b");
>     }
> 
> The two are subtly different, and the difference becomes apparent if we
> want to normalize arguments or make defensive copies, not just validate:
> 
> public B {
>         if (b <= 0)
>             b = 0;
>     }
> 
> If we put our implicit construction at the beginning, this would be a
> dead assignment to the parameter, after the record was initialized,
> which is almost certainly not what the user meant.  If we put it at the
> end, this would pick up the update.  The former seems pretty
> error-prone, so the latter seems attractive.
> 
> However, this runs into another issue, which is: what if we have
> additional fields?  (We might disallow this, but we might not.)  Now
> what if we wanted to do:
> 
>     record B(int a, int b) {
>         int cachedSum;
> 
>         B {
>             cachedSum = a + b;
>         }
>     }
> 
> If we treat the explicit statements as occuring before the default
> initialization, now `this` is DU at the point of assigning `cachedSum`,
> and the compiler tells us that we can't do this.  Of course, there's a
> workaround:
> 
> B {
> default.this(a, b);
> cachedSum = a + b;
>         }
> 
> which might be good enough. (Note that we'd like to be able to extend
> this ability to constructors of classes other than records eventually,
> so we should work out the construction protocol in generality even if
> we're not going to do it all now.)
> 
> Is `default.this(a, b)` still too verbose/general/error-prone?  Would
> some more invariant marker ("do the default thing now") be better, like:
> 
>     B {
>         new;
> this.cachedSum = a + b;
>     }
> 
> 
> 
> So, summarizing:
>  - We're OK with Foo { ... } as shorthand for the default constructor?
>  - Where should the implicit construction go -- beginning or end?
>  - Should there be a better idiom other than default.this(args) for "do
> the explicit construction now"?


More information about the amber-spec-experts mailing list