[
https://issues.apache.org/jira/browse/GROOVY-12115?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Paul King updated GROOVY-12115:
-------------------------------
Description:
h1. Summary
Introduce a parameter annotation (working name {{@ClassTag}}) that lets the
static
compiler *auto-supply a reified {{Class<K>}} argument* from a method receiver's
generic
type, so callers can omit type tokens that today exist only to work around
erasure.
Split off from GROOVY-11807. That issue ships the *runtime* half — explicit
{{Class<K>}}-token overloads that enforce types at run time. This issue is the
*compile-time ergonomics* half: making the token optional under static typing.
h2. Background and origin
On the JVM, a method that needs the runtime type of a generic type parameter
cannot
recover it (erasure), so the API must take an explicit {{Class<K>}} token.
Groovy
already does this in several places, e.g. {{asChecked}} (since 5.0.0) and the
new
key-checked {{withDefault}} overloads from GROOVY-11807:
{code:java}
// DefaultGroovyMethods
public static <T> Collection<T> asChecked(Collection<T> self, Class<T> type)
// -> Collections.checkedCollection
public static <K,V> Map<K,V> asChecked(Map<K,V> self, Class<K> keyType,
Class<V> valueType) // -> Collections.checkedMap
// GROOVY-11807 (already on the groovy11807 branch), Class params carry a /*
@ClassTag GROOVY-12115 */ marker
public static <K,V> Map<K,V> withDefault(Map<K,V> self, Class<K> keyType,
Class<V> valueType, Closure<V> init)
public static <K,V> Map<K,V> withDefault(Map<K,V> self, Class<K> keyType,
Closure<V> init)
{code}
In every case the {{Class}} argument is *redundant with a type the compiler
already
knows* (the receiver's type argument). {{@ClassTag}} removes the redundancy
under
static compilation.
h2. The problem
* Verbose: {{list.asChecked(String)}} / {{map.asChecked(Number, Foo)}} restate
the
element/key/value types the static type already declares.
* Not transparent: the safety-improving variant of {{withDefault}} can't be
selected
unless the caller manually threads the {{Class}}.
* The tokens cannot be inferred at run time — only the compiler, with the
static type
in hand, can supply them.
h2. Proposal: {{@ClassTag}}
A parameter annotation marking a {{Class<X>}} parameter as *compiler-supplied*.
Under
{{@CompileStatic}} / {{@TypeChecked}} the type checker may select an overload
whose only
extra parameter is a {{@ClassTag Class<X>}} and synthesise {{X.class}} from the
receiver's matching type argument, rewriting the call. Dynamic Groovy ignores
the
annotation and binds the original overload.
{code:java}
@Retention(RetentionPolicy.CLASS) // visible to the type checker; SOURCE may
suffice
@Target(ElementType.PARAMETER)
public @interface ClassTag {
// Optional override naming the type variable to reify, for when the
parameter
// type cannot carry it. NOTE: 'for' is a Java reserved word and cannot be
an
// annotation member name, so use e.g. value()/from(): @ClassTag(from =
"K")
String from() default "";
}
{code}
h3. Annotation shape and binding
* *Param-level, not method+index.* An earlier sketch put {{@OverrideCall(1)}}
on the
method with an index. Moving the marker onto the parameter makes the *position*
implicit (where it sits) and lets the *parameter type* {{Class<K>}} encode
*which*
generic to reify — strictly more self-describing than a bare index (which never
said
K-vs-V).
* *Default binding* reads the type variable from {{Class<X>}}. The type checker
sees
{{<X>}} pre-erasure (and the Signature attribute retains it), so this is
reliable.
* *Optional override* {{@ClassTag(from="K")}} for edge cases the parameter type
can't
express — a {{Class<?>}}/raw token, or a method that declares its own type
variables
that don't line up with the receiver's. Because it is a String, the STC must
*reject a
{{from}} that does not name a resolvable type variable* so typos fail at
compile time.
h3. Multiple tokens and fill order
Some consumers need more than one token, e.g. {{asChecked(Map, @ClassTag
Class<K>,
@ClassTag Class<V>)}}. Rule: each {{@ClassTag}} parameter is synthesised *at its
declared position*; the caller's supplied arguments map to the remaining
(non-tag)
parameters left-to-right. For {{asChecked()}} that means "fill slots 1 and 2
with
{{K.class}}, {{V.class}} in declaration order." (This generalises the
single-hole case
the {{@OverrideCall(1)}} index handled.)
h3. Overload selection / preemption
For a call that *already matches* a no-token overload (e.g.
{{withDefault{...}}}), the
{{@ClassTag}} overload must *preempt* it for a transparent upgrade — the
compiler
prefers injecting a token over an exact arity match. This is a deliberate
resolution
rule and means unchanged source can change behaviour under static typing.
* Use preemption *only when the new behaviour is an unambiguous bug-fix* (the
old
behaviour has no legitimate users).
* Otherwise *mint a new name* (e.g. {{withCheckedDefault}}) so the opt-in is by
name
and nothing existing changes meaning. {{@ClassTag}} still earns its keep there
by
letting static callers omit the {{Class}}.
h2. Examples (before / after, under @CompileStatic)
||API||Today||With @ClassTag||
|asChecked (list)|{{[].asChecked(String)}}|{{[].asChecked()}}|
|asChecked (map)|{{[:].asChecked(Number, Foo)}}|{{[:].asChecked()}}|
|withDefault (map)|{{[:].withDefault(Number){...}}}|{{[:].withDefault{...}}}
(transparently checked)|
{code:groovy}
// asChecked: the Class was mandatory -> @ClassTag makes it omissible (opt-in,
additive)
Map<Number,String> m = [:].asChecked() // compiler injects
Number.class, Foo.class
// withDefault: a no-Class form already exists -> @ClassTag overload preempts
it (silent upgrade)
Map<Number,?> m = [:].withDefault{ null } // same source as today
m['x'] // now ClassCastException
(compiler injected Number.class)
{code}
h2. Boundaries and edges
* *Static-only.* The injection is an STC rule. In dynamic Groovy:
** {{withDefault{}}} keeps binding the *unchecked* overload (no K to inject) —
same
source, different behaviour by compilation mode.
** {{asChecked()}} (no Class) won't resolve at all — there is no zero-arg
method and
no injection -> {{MissingMethodException}}. The shorter spelling is
*static-only syntax*.
* *K must be statically known.* {{Map<Number,?>}} works; {{def}} / raw {{Map}} /
wildcard-only don't — no token is synthesised, so {{withDefault{}}} stays
lenient and
{{asChecked()}} won't compile.
* *Fail-soft vs fail-loud.* As sketched, when K can't be reified the compiler
silently
degrades (no token / {{Object.class}}). Scala instead makes it a *compile
error* (you
must propagate the evidence). Degrading soft risks a false sense of safety;
this needs
a decision (see open questions).
* *Escape hatch.* The explicit {{Class}} form always works — in every mode, and
when
the type is unknown.
* *Fidelity is erased.* {{Class<K>}} reifies only the *erased* class:
{{List<String>}} and {{List<Integer>}} are indistinguishable (see TypeTag
section).
h2. Retrofit candidates (codebase audit)
||Tier||API||Why it fits / note||
|1|{{asChecked(...)}} — 9 overloads, since 5.0.0|Textbook case: explicit
{{Class}} is purely an erasure-workaround over {{Collections.checkedXxx}}. Also
the multi-token (K,V) stress test.|
|1|{{withDefault}} / {{withCheckedDefault}} (GROOVY-11807)|Type-laundering
insert via the untyped {{Map.get(Object)}}; reified K lets it check. Unifies
with checked maps: {{withDefault = checkedMap + default}} once K is reified.|
|2|{{ListWithDefault}} ({{(T) initClosure.call(...)}})|Default cast to element
type; weaker — the closure return is already statically checkable.|
|3 (new method)|{{collection.toArray()}} -> {{T[]}}|Reified array creation (the
Kotlin {{reified}} / Scala {{ClassTag}} array case). New API, not a retrofit of
{{asType}}.|
|none|{{asType}}/{{as}}, {{newInstance(Class)}}, {{find}}/{{grep}}-by-Class,
{{castToType}}|The {{Class}} is the user's *intent*, not an erasure-workaround
— auto-supplying would be wrong.|
Net: the realistic footprint is small and clustered around *checked-view and
default-growing collection APIs*, not a sprawling refactor.
h2. Reification fidelity: ClassTag vs a possible TypeTag tier (weaker)
{{@ClassTag}} supplies an *erased* {{Class<K>}}. A richer future tier could
inject a
*full-generic* token (a super-type token {{TypeToken<K>}}), analogous to Scala's
{{TypeTag}} / {{scala.quoted.Type}} versus its {{ClassTag}}. Kotlin's
{{inline fun <reified T>}} is the JVM precedent for the cheap tier.
*This extension now looks weak given the boundaries above:*
* The concrete candidates ({{asChecked}} -> {{Collections.checkedMap}},
{{withDefault}})
only ever do {{isInstance}} checks, which are erased anyway — the erased class
suffices.
* Restricting the source to the *receiver's type argument* and to *static-only*
injection further limits where full-generic fidelity would pay off.
Recommendation: keep {{ClassTag}} (erased) as the design; note
{{TypeTag}}/{{TypeToken}}
as a *possible later tier* only if a real full-generic consumer appears. The
annotation
*name* could encode the fidelity ({{@ClassTag}} now, reserving {{@TypeTag}} for
the full
tier), or favour familiarity ({{@Reified}}, after Kotlin) — see open questions.
h2. Comparison with Scala / Kotlin / Java
||Mechanism||How the type is obtained||Fidelity||Propagation||
|Groovy {{@ClassTag}} (proposed)|compiler synthesises {{Class}} from the
receiver's type arg|erased class|fail-soft (proposed)|
|Scala {{ClassTag}}|implicit/{{using}} evidence materialised from the static
type|erased class|fail-loud (context bound {{[T: ClassTag]}})|
|Scala {{TypeTag}} / {{quoted.Type}}|implicit/{{using}} evidence|full
generic|fail-loud|
|Kotlin {{reified}}|{{inline}} + reify the type parameter|actual type
(inlined)|enforced by {{inline}}|
|Java baseline|explicit {{Class<T>}} token ({{EnumMap}},
{{Collections.checkedMap}}, {{getAnnotation(Class<T>)}})|erased class|manual|
|Guava/Gson {{TypeToken<T>}}|super-type token|full generic|manual|
In short: {{@ClassTag}} is Scala's {{ClassTag}} idea (compiler-supplied runtime
type
evidence) delivered via an annotation + STC call-rewrite instead of implicit
parameters; it auto-supplies the *Java {{Class<T>}} token* idiom rather than
inventing
a new runtime representation.
h2. Proposed scope / deliverables
# The {{@ClassTag}} annotation (parameter target; CLASS/SOURCE retention;
optional
{{from}}/{{value}} override — *not* {{for}}, which is reserved).
# STC support: overload selection that prefers a {{@ClassTag}} overload and
synthesises
the {{X.class}} argument(s) from the receiver's type arguments; validation of
{{from}}; well-defined multi-token fill order.
# First consumers: retrofit {{asChecked}} and the GROOVY-11807 {{withDefault}}
overloads (validating the K,V multi-token case).
# Decide and document: fail-soft vs fail-loud; preemption-vs-new-name policy.
*Out of scope:* the full-generic {{TypeTag}}/{{TypeToken}} tier; any change to
dynamic
dispatch (dynamic callers keep passing the {{Class}} explicitly).
h2. Open questions
* *Fail-soft vs fail-loud* when K is not statically reifiable (silent degrade vs
compile error, à la Scala context bounds).
* *Resolution precedence:* exact rule for when a {{@ClassTag}} overload
preempts a
matching no-token overload, and the guidance for choosing a new name instead.
* *Annotation name:* {{@ClassTag}} (honest about erased fidelity) vs
{{@Reified}}
(familiar, Kotlin) vs reserving {{@TypeTag}} for a later full tier.
* *Partial supply:* may a caller pass some tokens explicitly and have the rest
synthesised (e.g. pass K, infer V)? Natural given fixed positions, but a rule
to state.
* *Retention / API status:* public annotation vs internal marker; ship
{{@Incubating}}.
h2. Related issues
* GROOVY-11807 — runtime key-checked {{withDefault}} (the quick-win this builds
on); its
new {{Class}} parameters already carry {{/* @ClassTag GROOVY-12115 */}} markers
as the
seam for this work.
was:
{code}
Map<Number,?> map = [:].withDefault{ null }
assert map.get('x') == null
Number k = map.keySet()[0]
{code}
GroovyCastException: Cannot cast object 'x' with class 'java.lang.String' to
class 'java.lang.Number'
> Reify a receiver's type argument as a Class parameter under @CompileStatic
> (@ClassTag)
> --------------------------------------------------------------------------------------
>
> Key: GROOVY-12115
> URL: https://issues.apache.org/jira/browse/GROOVY-12115
> Project: Groovy
> Issue Type: Bug
> Components: groovy-jdk
> Reporter: Eric Milles
> Priority: Major
>
> h1. Summary
> Introduce a parameter annotation (working name {{@ClassTag}}) that lets the
> static
> compiler *auto-supply a reified {{Class<K>}} argument* from a method
> receiver's generic
> type, so callers can omit type tokens that today exist only to work around
> erasure.
> Split off from GROOVY-11807. That issue ships the *runtime* half — explicit
> {{Class<K>}}-token overloads that enforce types at run time. This issue is the
> *compile-time ergonomics* half: making the token optional under static typing.
> h2. Background and origin
> On the JVM, a method that needs the runtime type of a generic type parameter
> cannot
> recover it (erasure), so the API must take an explicit {{Class<K>}} token.
> Groovy
> already does this in several places, e.g. {{asChecked}} (since 5.0.0) and the
> new
> key-checked {{withDefault}} overloads from GROOVY-11807:
> {code:java}
> // DefaultGroovyMethods
> public static <T> Collection<T> asChecked(Collection<T> self, Class<T>
> type) // -> Collections.checkedCollection
> public static <K,V> Map<K,V> asChecked(Map<K,V> self, Class<K> keyType,
> Class<V> valueType) // -> Collections.checkedMap
> // GROOVY-11807 (already on the groovy11807 branch), Class params carry a /*
> @ClassTag GROOVY-12115 */ marker
> public static <K,V> Map<K,V> withDefault(Map<K,V> self, Class<K> keyType,
> Class<V> valueType, Closure<V> init)
> public static <K,V> Map<K,V> withDefault(Map<K,V> self, Class<K> keyType,
> Closure<V> init)
> {code}
> In every case the {{Class}} argument is *redundant with a type the compiler
> already
> knows* (the receiver's type argument). {{@ClassTag}} removes the redundancy
> under
> static compilation.
> h2. The problem
> * Verbose: {{list.asChecked(String)}} / {{map.asChecked(Number, Foo)}}
> restate the
> element/key/value types the static type already declares.
> * Not transparent: the safety-improving variant of {{withDefault}} can't be
> selected
> unless the caller manually threads the {{Class}}.
> * The tokens cannot be inferred at run time — only the compiler, with the
> static type
> in hand, can supply them.
> h2. Proposal: {{@ClassTag}}
> A parameter annotation marking a {{Class<X>}} parameter as
> *compiler-supplied*. Under
> {{@CompileStatic}} / {{@TypeChecked}} the type checker may select an overload
> whose only
> extra parameter is a {{@ClassTag Class<X>}} and synthesise {{X.class}} from
> the
> receiver's matching type argument, rewriting the call. Dynamic Groovy ignores
> the
> annotation and binds the original overload.
> {code:java}
> @Retention(RetentionPolicy.CLASS) // visible to the type checker; SOURCE
> may suffice
> @Target(ElementType.PARAMETER)
> public @interface ClassTag {
> // Optional override naming the type variable to reify, for when the
> parameter
> // type cannot carry it. NOTE: 'for' is a Java reserved word and cannot
> be an
> // annotation member name, so use e.g. value()/from(): @ClassTag(from =
> "K")
> String from() default "";
> }
> {code}
> h3. Annotation shape and binding
> * *Param-level, not method+index.* An earlier sketch put {{@OverrideCall(1)}}
> on the
> method with an index. Moving the marker onto the parameter makes the
> *position*
> implicit (where it sits) and lets the *parameter type* {{Class<K>}} encode
> *which*
> generic to reify — strictly more self-describing than a bare index (which
> never said
> K-vs-V).
> * *Default binding* reads the type variable from {{Class<X>}}. The type
> checker sees
> {{<X>}} pre-erasure (and the Signature attribute retains it), so this is
> reliable.
> * *Optional override* {{@ClassTag(from="K")}} for edge cases the parameter
> type can't
> express — a {{Class<?>}}/raw token, or a method that declares its own type
> variables
> that don't line up with the receiver's. Because it is a String, the STC must
> *reject a
> {{from}} that does not name a resolvable type variable* so typos fail at
> compile time.
> h3. Multiple tokens and fill order
> Some consumers need more than one token, e.g. {{asChecked(Map, @ClassTag
> Class<K>,
> @ClassTag Class<V>)}}. Rule: each {{@ClassTag}} parameter is synthesised *at
> its
> declared position*; the caller's supplied arguments map to the remaining
> (non-tag)
> parameters left-to-right. For {{asChecked()}} that means "fill slots 1 and 2
> with
> {{K.class}}, {{V.class}} in declaration order." (This generalises the
> single-hole case
> the {{@OverrideCall(1)}} index handled.)
> h3. Overload selection / preemption
> For a call that *already matches* a no-token overload (e.g.
> {{withDefault{...}}}), the
> {{@ClassTag}} overload must *preempt* it for a transparent upgrade — the
> compiler
> prefers injecting a token over an exact arity match. This is a deliberate
> resolution
> rule and means unchanged source can change behaviour under static typing.
> * Use preemption *only when the new behaviour is an unambiguous bug-fix* (the
> old
> behaviour has no legitimate users).
> * Otherwise *mint a new name* (e.g. {{withCheckedDefault}}) so the opt-in is
> by name
> and nothing existing changes meaning. {{@ClassTag}} still earns its keep
> there by
> letting static callers omit the {{Class}}.
> h2. Examples (before / after, under @CompileStatic)
> ||API||Today||With @ClassTag||
> |asChecked (list)|{{[].asChecked(String)}}|{{[].asChecked()}}|
> |asChecked (map)|{{[:].asChecked(Number, Foo)}}|{{[:].asChecked()}}|
> |withDefault (map)|{{[:].withDefault(Number){...}}}|{{[:].withDefault{...}}}
> (transparently checked)|
> {code:groovy}
> // asChecked: the Class was mandatory -> @ClassTag makes it omissible
> (opt-in, additive)
> Map<Number,String> m = [:].asChecked() // compiler injects
> Number.class, Foo.class
> // withDefault: a no-Class form already exists -> @ClassTag overload preempts
> it (silent upgrade)
> Map<Number,?> m = [:].withDefault{ null } // same source as today
> m['x'] // now ClassCastException
> (compiler injected Number.class)
> {code}
> h2. Boundaries and edges
> * *Static-only.* The injection is an STC rule. In dynamic Groovy:
> ** {{withDefault{}}} keeps binding the *unchecked* overload (no K to inject)
> — same
> source, different behaviour by compilation mode.
> ** {{asChecked()}} (no Class) won't resolve at all — there is no zero-arg
> method and
> no injection -> {{MissingMethodException}}. The shorter spelling is
> *static-only syntax*.
> * *K must be statically known.* {{Map<Number,?>}} works; {{def}} / raw
> {{Map}} /
> wildcard-only don't — no token is synthesised, so {{withDefault{}}} stays
> lenient and
> {{asChecked()}} won't compile.
> * *Fail-soft vs fail-loud.* As sketched, when K can't be reified the compiler
> silently
> degrades (no token / {{Object.class}}). Scala instead makes it a *compile
> error* (you
> must propagate the evidence). Degrading soft risks a false sense of safety;
> this needs
> a decision (see open questions).
> * *Escape hatch.* The explicit {{Class}} form always works — in every mode,
> and when
> the type is unknown.
> * *Fidelity is erased.* {{Class<K>}} reifies only the *erased* class:
> {{List<String>}} and {{List<Integer>}} are indistinguishable (see TypeTag
> section).
> h2. Retrofit candidates (codebase audit)
> ||Tier||API||Why it fits / note||
> |1|{{asChecked(...)}} — 9 overloads, since 5.0.0|Textbook case: explicit
> {{Class}} is purely an erasure-workaround over {{Collections.checkedXxx}}.
> Also the multi-token (K,V) stress test.|
> |1|{{withDefault}} / {{withCheckedDefault}} (GROOVY-11807)|Type-laundering
> insert via the untyped {{Map.get(Object)}}; reified K lets it check. Unifies
> with checked maps: {{withDefault = checkedMap + default}} once K is reified.|
> |2|{{ListWithDefault}} ({{(T) initClosure.call(...)}})|Default cast to
> element type; weaker — the closure return is already statically checkable.|
> |3 (new method)|{{collection.toArray()}} -> {{T[]}}|Reified array creation
> (the Kotlin {{reified}} / Scala {{ClassTag}} array case). New API, not a
> retrofit of {{asType}}.|
> |none|{{asType}}/{{as}}, {{newInstance(Class)}}, {{find}}/{{grep}}-by-Class,
> {{castToType}}|The {{Class}} is the user's *intent*, not an
> erasure-workaround — auto-supplying would be wrong.|
> Net: the realistic footprint is small and clustered around *checked-view and
> default-growing collection APIs*, not a sprawling refactor.
> h2. Reification fidelity: ClassTag vs a possible TypeTag tier (weaker)
> {{@ClassTag}} supplies an *erased* {{Class<K>}}. A richer future tier could
> inject a
> *full-generic* token (a super-type token {{TypeToken<K>}}), analogous to
> Scala's
> {{TypeTag}} / {{scala.quoted.Type}} versus its {{ClassTag}}. Kotlin's
> {{inline fun <reified T>}} is the JVM precedent for the cheap tier.
> *This extension now looks weak given the boundaries above:*
> * The concrete candidates ({{asChecked}} -> {{Collections.checkedMap}},
> {{withDefault}})
> only ever do {{isInstance}} checks, which are erased anyway — the erased
> class suffices.
> * Restricting the source to the *receiver's type argument* and to
> *static-only*
> injection further limits where full-generic fidelity would pay off.
> Recommendation: keep {{ClassTag}} (erased) as the design; note
> {{TypeTag}}/{{TypeToken}}
> as a *possible later tier* only if a real full-generic consumer appears. The
> annotation
> *name* could encode the fidelity ({{@ClassTag}} now, reserving {{@TypeTag}}
> for the full
> tier), or favour familiarity ({{@Reified}}, after Kotlin) — see open
> questions.
> h2. Comparison with Scala / Kotlin / Java
> ||Mechanism||How the type is obtained||Fidelity||Propagation||
> |Groovy {{@ClassTag}} (proposed)|compiler synthesises {{Class}} from the
> receiver's type arg|erased class|fail-soft (proposed)|
> |Scala {{ClassTag}}|implicit/{{using}} evidence materialised from the static
> type|erased class|fail-loud (context bound {{[T: ClassTag]}})|
> |Scala {{TypeTag}} / {{quoted.Type}}|implicit/{{using}} evidence|full
> generic|fail-loud|
> |Kotlin {{reified}}|{{inline}} + reify the type parameter|actual type
> (inlined)|enforced by {{inline}}|
> |Java baseline|explicit {{Class<T>}} token ({{EnumMap}},
> {{Collections.checkedMap}}, {{getAnnotation(Class<T>)}})|erased class|manual|
> |Guava/Gson {{TypeToken<T>}}|super-type token|full generic|manual|
> In short: {{@ClassTag}} is Scala's {{ClassTag}} idea (compiler-supplied
> runtime type
> evidence) delivered via an annotation + STC call-rewrite instead of implicit
> parameters; it auto-supplies the *Java {{Class<T>}} token* idiom rather than
> inventing
> a new runtime representation.
> h2. Proposed scope / deliverables
> # The {{@ClassTag}} annotation (parameter target; CLASS/SOURCE retention;
> optional
> {{from}}/{{value}} override — *not* {{for}}, which is reserved).
> # STC support: overload selection that prefers a {{@ClassTag}} overload and
> synthesises
> the {{X.class}} argument(s) from the receiver's type arguments; validation of
> {{from}}; well-defined multi-token fill order.
> # First consumers: retrofit {{asChecked}} and the GROOVY-11807 {{withDefault}}
> overloads (validating the K,V multi-token case).
> # Decide and document: fail-soft vs fail-loud; preemption-vs-new-name policy.
> *Out of scope:* the full-generic {{TypeTag}}/{{TypeToken}} tier; any change
> to dynamic
> dispatch (dynamic callers keep passing the {{Class}} explicitly).
> h2. Open questions
> * *Fail-soft vs fail-loud* when K is not statically reifiable (silent degrade
> vs
> compile error, à la Scala context bounds).
> * *Resolution precedence:* exact rule for when a {{@ClassTag}} overload
> preempts a
> matching no-token overload, and the guidance for choosing a new name instead.
> * *Annotation name:* {{@ClassTag}} (honest about erased fidelity) vs
> {{@Reified}}
> (familiar, Kotlin) vs reserving {{@TypeTag}} for a later full tier.
> * *Partial supply:* may a caller pass some tokens explicitly and have the rest
> synthesised (e.g. pass K, infer V)? Natural given fixed positions, but a rule
> to state.
> * *Retention / API status:* public annotation vs internal marker; ship
> {{@Incubating}}.
> h2. Related issues
> * GROOVY-11807 — runtime key-checked {{withDefault}} (the quick-win this
> builds on); its
> new {{Class}} parameters already carry {{/* @ClassTag GROOVY-12115 */}}
> markers as the
> seam for this work.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)