Ad hoc type restriction
Swaranga Sarma
sarma.swaranga at gmail.com
Tue Oct 14 01:17:05 UTC 2025
Yes, this would need to be a proper language feature. Some time back in a
Reddit wishlist for Java features I had posted this as a "type aliases with
validation" feature. Something like a new language construct to declare a
new type with an accompanying validation lambda:
// definition site
type-alias CustomerId::String where {
if (!this.matches("CUST-[0-9]{4}")) {
throw new IllegalArgumentException("CustomerId must match pattern CUST-XXXX
where X is a digit.");
}
}
// use site
void handleCustomer(CustomerId id) {
// id guaranteed to match pattern CUST-XXXX
// can call all String methods
}
CustomerId id = "CUST-1234"; // valid; validation executed at assignment
time
String invalidId = "INVALID";
String validId = "CUST-1234";
handleCustomer(id); // validation executed at runtime exactly once
handleCustomer(validId); // validation executed at runtime exactly once
handleCustomer(invalidId); // throws IllegalArgumentException at runtime
I only wrote it to demonstrate what I was talking about; it wasn't a
suggestion on what it should look like or how the validation that
would happen at different sites be surfaced to the user. So please
disregard the syntax. What I really wanted to say was that this would be a
game-changer for so many of my projects. Many times, I am tempted to take
the shortcut of not creating the wrapper domain types to avoid the
allocation but also to avoid having to lift some of the methods from the
wrapped type to the wrapper. Valhalla may address the allocation concern
though but something like this would be amazing.
I will go read about type classes as I know nothing about them.
Regards
Swaranga
On Mon, Oct 13, 2025 at 5:50 PM Brian Goetz <brian.goetz at oracle.com> wrote:
> I just want to point out that there's a sort of "syntax error" in your
> proposal.
>
> Java provides annotations as a means of "structured comments" on
> declarations and type uses, but the Java language does not, and will not,
> impart any semantics to programs on the basis of annotations. If you are
> talking about writing a static analysis tool, perhaps a pluggable checker
> in the Checkers framework, then (as Ethan points out) you can use existing
> annotations with APs and do so, and the compiler is merely a conduit for
> ferrying the annotations to where an AP can find them. (In fact, there is
> already a checker for "fake enums", where you say that a given `int` is
> really one of the enumerated set 1, 2, 3, 4, which is a restriction type.)
>
> If you mean that the compiler actually is going to get into the act,
> though, then this is not an annotation-driven feature, this is a full-blown
> language feature and should be thought of accordingly. I know its tempting
> to view annos as a "shortcut" to language features, but if something has
> semantics, its part of the language, and sadly that means no shortcuts.
> That's not to say it isn't a worthwhile idea with a good cost-to-benefit
> ratio. (Indeed, as we get further into the type classes work, the logic of
> a `newtype` mechanism becomes even more compelling as then it becomes
> possible to affect behavior with restrictions such as
> `CaseInsensitveString`, which doesn't actually restrict the value set of
> the type, but allows you to define `Ord CaseInsentiveString` separate from
> `Ord String`.)
>
>
>
> On 10/13/2025 3:17 PM, Archie Cobbs wrote:
>
> Ethan McCue <ethan at mccue.dev> wrote:
>
>> However there is nothing conceptually preventing the tools validating
>> @NonNull usage from also emitting an error until you have inserted a known
>> precheck.
>> ...
>> But for other single-value invariants, like your @PhoneNumber example, it
>> seems fairly practical. Especially since, as a general rule, arbitrary cost
>> computations really shouldn't be invisible. How would one know if (@B A) is
>> going to thread invocations of some validation method everywhere?
>
>
> This is why 3rd party tools aren't as good as having the compiler handle
> it, because the compiler is in a position to provide both stronger and more
> efficient guarantees - think generic types and runtime erasure.
> Compiler-supported typing allows the developer to move the burden of proof
> from the method receiving a parameter to the code invoking that method, and
> onward back up the call chain, so that validations tend to occur "early",
> when they are first known to be true, instead of "late" at the (many more)
> points in the code where someone actually cares that they are true.
>
> So if phone numbers are central to your application, and they are passed
> around and used all over the place as type @PhoneNumber String, then they
> will only need to actually be validated at a few application entry points,
> not at the start of every method that has a phone number as a parameter. In
> other words, the annotation is ideally not a "to-do" list but rather an
> "it's already done" list.
>
> The guarantee that the compiler would then provide is ideally on the same
> level as with generics: while it's being provided by the compiler, not the
> JVM, so you can always get around it if you try hard enough (native code,
> reflection, class file switcheroo, etc.), as long as you "follow the rules"
> you get the guarantee - or if not, an error or at least a warning.
>
> Brian Goetz <brian.goetz at oracle.com> wrote:
>
>> I think the best bet for making this usable would be some mechanism like
>> a "view", likely only on value types, that would erase down to the
>> underlying wrapped type, but interpose yourself on construction, and
>> provided a conversion from T to RefinedT that verified the requirement.
>> But this is both nontrivial and presumes a lot of stuff we don't even have
>> yet...
>>
>
> I think that is close to what I was imagining. It seems like it could be
> done with fairly minimal impact/disruption...? No need for wrappers or
> views.
>
> But first just to be clear, what I'm getting at here is a fairly narrow
> idea, i.e., what relatively simple thing might the compiler do, with a
> worthwhile cost/benefit ratio, to make it easier for developers to reason
> about the correctness of their code when "type restriction" is being used,
> either formally or informally (meaning, if you're using an int to pass
> around the size of collection, you're doing informal type restriction).
>
> What's the benefit? Type restriction is fairly pervasive, and yet because
> Java doesn't make it very easy to do, it's often not being done at all, and
> this ends up adding to the amount of manual work developers must do to
> prove to themselves their code is correct. The more of this burden the
> compiler could take on, the bigger the benefit would be.
>
> What's the cost? That depends on the solution of course.
>
> To me the giant poster-child for this kind of pragmatic language addition
> is generics. It had all kinds of minor flaws from the point of view of
> language design, but the problem it addressed was so pervasive, and the new
> tool it provided to developers for verifying the correctness of their code
> was so powerful, that nobody thinks it wasn't worth the trade-off.
>
> OK let me throw out two straw-man proposals. I'll just assume these are
> stupid/naive ideas with major flaws. Hopefully they can at least help map
> out the usable territory - if any exists.
>
> *Proposal #1*
>
> This one is very simple, but provides a weaker guarantee.
>
> 1. The compiler recognizes and tracks "type restriction annotations",
> which are type annotations having the meta-annotation @TypeRestriction
> 2. For all operations assigning some value v of type S to type T:
> 1. If a type restriction annotation A is present on T but not S,
> the compiler generates a warning in the new lint category
> "type-restriction"
>
> That's it. A cast like var pn = (@PhoneNumber String)input functions
> simply as a developer assertion that the type restriction has been
> verified, but the compiler does not actually check this. There is no change
> to the generated bytecode. If the developer chooses to write a validation
> method that takes a string, validates it (or throws an exception), and then
> returns the validated string, that method will need to be annotated with
> @SuppressWarnings("type-restriction") because of the cast in front of the
> return statement.
>
> Guarantee provided: Proper type restriction as long as "type-restriction"
> warnings are enabled and not emitted. However, this is a "fail slow"
> guarantee: it's easy to defeat (just cast!). So if you write a method that
> takes a @PhoneNumber String parameter that is passed an invalid value,
> you won't find out until something goes wrong later down the line (or
> never). In other words, *your* code will be correct, but you have to be
> trusting of any code that *invokes* your code, which in practice is not
> always a sound strategy.
>
> *Proposal #2*
>
> This is proposal is more complex but provides a stronger guarantee:
>
> 1. The compiler recognizes and tracks "type restriction annotations",
> which have the meta-annotation @TypeRestriction
> 1. The annotation specifies a user-supplied "constructor" class
> providing a user-defined construction/validation method validate(v)
> 2. We add class TypeRestrictionException extends RuntimeException and
> encourage validate() methods to throw (some subclass of) it
> 2. For all operations assigning some value v of type S to type T:
> 1. If a type restriction annotation A is present on T but not S,
> the compiler generates a "type-restriction" warning AND adds an
> implicit cast added (see next step)
> 3. For every cast like var pn = (@PhoneNumber String)"+15105551212" the
> compiler inserts bytecode to invoke the appropriate enforcer
> validate(v) method
> 4. The JLS rules for method resolution, type inference, etc., do not
> change (that would be way over-complicating things)
> 1. Two methods void dial(String pn) and void dial(@PhoneNumber
> String pn) will still collide
>
> Guarantee provided: Proper type restriction unless you are going to
> extremes (native code, reflection, runtime classfile switcheroo, etc.).
> This is a "fail fast" guarantee: errors are caught at the moment an invalid
> value is assigned to a type-restricted variable. If your method parameters
> have the annotation, you don't have to trust 3rd party code that calls
> those methods (as long as it was compiled properly). I.e., same level of
> guarantee as generics.
>
> These are by no means complete or particularly elegant solutions from a
> language design point of view. They are pragmatic and relatively
> unobtrusive add-ons, using existing language concepts, to get us most of
> what we want, which is:
>
> - User-defined "custom" type restrictions with compile-time
> checking/enforcement
> - As with generics, the goal is not language perfection, but rather
> making it easier for developers to reason about correctness
> - Compile-time guarantees that type restricted values in source files
> will be actually type restricted at runtime
> - Efficient implementation
> - Validation only happens "when necessary"
> - No JVM changes needed (erasure)
> - No changes to language syntax; existing source files are 100%
> backward compatible
>
> The developer side of me says that the cost/benefit ratio of something
> like this would be worthwhile, in spite of its pragmatic nature, simply
> because the problem being addressed seems so pervasive. I felt the same way
> about generics (which was a much bigger change addressing a
> much bigger pervasive problem).
>
> But I'm sure there are things I'm missing... ?
>
> -Archie
>
> --
> Archie L. Cobbs
>
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20251013/fd3ac03c/attachment-0001.htm>
More information about the amber-dev
mailing list