Ad hoc type restriction
Chris Bouchard
chris at upliftinglemma.net
Mon Oct 13 21:56:54 UTC 2025
Archie,
I think first-party type restrictions would be great! My first thought
was that these annotation-based type restrictions we're discussing
feel very similar to constraint annotations in Jakarta Validation,
a.k.a. Bean Validation or Hibernate Validation. (I don't think I've
seen that mentioned yet in the thread.) I don't think Jakarta
Validation fits the use case we're exploring here, but I'm sure there
are some lessons to be learned from its design.
I have a couple thoughts regarding Proposal #2.
On Mon, Oct 13, 2025 at 3:19 PM Archie Cobbs <archie.cobbs at gmail.com> wrote:
>
> Proposal #2
>
> This is proposal is more complex but provides a stronger guarantee:
>
> ...
>
> 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)
I think it's worth calling out that restricted types could be more
complex than @PhoneNumber String. For example, we could (presumably)
have a restricted type like List<@Directory Map<@Name String,
@PhoneNumber String>>, where different annotations are attached to
different "nodes" of the generic type.
Further, it feels natural that we'd want to use type restriction
annotations in class definitions like
public class NameMap<V> implements Map<@Name String, V> { ... }
so that the type restriction is present in the inherited interface
methods.* In that case, we'd need NameMap<@PhoneNumber String> to
match Map<@Name String, @PhoneNumber String>. None of this is
difficult or new, but it is more complex than just checking top-level
type annotations.
(* I originally followed this with, "Or else users would have to
override every inherited method to add the annotation manually." But
that would actually run afoul of variance, right? Assuming we think of
@PhoneNumber String as a subtype of String.)
> 3. For every cast like var pn = (@PhoneNumber String)"+15105551212" the compiler inserts bytecode to invoke the appropriate enforcer validate(v) method
I like that this proposal forces a validation in order to apply the
restriction, but I don't think I'm a fan of hanging it off of
casting—that feels too magical to me. I get that we want a certain
level of magic, because we want this to be painless for the end user,
but I think it's a reasonable assumption right now that casting is
"almost free." It's sort of the same complaint as with operator
overloading. Having casts run library code feels to me like a footgun
waiting to happen.
Instead, I'd suggest that validators could just be library methods
with their own annotation. E.g.,
public class Validators {
@TypeRestrictionValidator(@PhoneNumber)
public static String phoneNumber(String value) {
if (!isValidPhoneNumber(value)) {
throw new TypeRestrictionException("Not a phone number!");
}
return value;
}
}
The compiler could notice the @TypeValidator annotation and check that
phoneNumber is a valid "type restriction validator method" matching a
functional interface like
public interface TypeRestrictionValidator<T> {
T validate(T value);
}
When calling a type restriction validator method, the compiler can
automatically apply the appropriate cast to the result *without*
triggering a warning—every other conversion to a "more restricted"
type would warn. (In a sense, @TypeRestrictionValidator(@PhoneNumber)
augments the return type from String to @PhoneNumber String.)
I suggest that validator methods return T instead of @Restriction
T—which would essentially be your Proposal #1—so that the validator
method doesn't have to suppress warnings in its own body, and so can
still benefit from checking for correct use of *other* restriction
types. This might promote a certain amount of composability. E.g., the
@PhoneNumber validator method could be
public static @NotBlank String phoneNumber(@NotBlank String value) { ... }
and it could internally call other methods that depend on that
@NotBlank restriction, secure in the knowledge that the compiler is
checking those calls. It's also important that the validator's return
type be "as restricted" as its input type, so that the compiler can
add the new restriction without invalidating the existing ones.
One small note: Specifying this with a functional interface would also
allow "fluent validation methods" via instance methods, if a library
author prefers that to static methods.
My suggestion is inspired by the TypeIs[T] type annotation in Python.
(And I'm sure there are similar concepts in other languages, but I
can't think of them off hand.) Note, though, that Python TypeIs
functions return a boolean indicating whether the value is a member of
the restricted type or not, which is used to narrow the type in the
calling scope. It doesn't seem as natural for Java to narrow based on
a conditional, so I suggested returning a value instead.
I think it would also be reasonable for the contract to be
public interface TypeRestrictionValidator<T> {
void validate(T value); // Return void instead of T.
}
so that the validator method can't replace the value. I do think
there's value in allowing the validator to replace the value—e.g., the
validator for @PhoneNumber could return a string in a normalized
format—but it's not *necessary*. But I also think that would be more
frustrating to use, because you'd have to call the validator outside
of any method chain or expression.
Thanks,
Chris Bouchard
More information about the amber-dev
mailing list