[ 
https://issues.apache.org/jira/browse/GROOVY-12205?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Paul King updated GROOVY-12205:
-------------------------------
    Description: 
GROOVY-12054, GROOVY-12034 and GROOVY-12043 introduced "fat-free" variants of 
common DGM methods accepting {{java.util.function}} types ({{Function}}, 
{{Predicate}}, {{BiFunction}}, ...) alongside the classic {{Closure}} 
overloads, so lambdas and functional values obtained from Java APIs can be used 
with Groovy's collection methods without wrapping. Coverage is currently 
partial, which makes the surface irregular: a user (or an AI coding assistant) 
who learns that {{collect}} accepts a {{Function}} will reasonably expect 
{{collectMany}} to do the same, and currently gets a {{MissingMethodException}}.

This umbrella issue proposes completing the surface to a predictable rule:

{quote}Every Closure-taking collection/map DGM method has a 
functional-interface twin, unless the JDK already provides the 
equivalent.{quote}

Methods currently without any functional variant (Closure-overload count in 
parens): {{collectMany}} (8), {{sum}} (6), {{takeWhile}} (6), {{dropWhile}} 
(6), {{countBy}} (4), {{split}} (5), {{flatten}} (4, the {{flattenUsing}} 
forms), {{injectAll}} (4), {{collectingMany}} (1). Partial coverage to round 
out: {{inject}} (2 of 9), {{groupBy}} (1 of 6), and the key-extractor forms of 
{{min}}/{{max}}.

Deliberate exception to confirm rather than implement: 
{{removeAll}}/{{retainAll}}, where JDK {{Collection#removeIf(Predicate)}} 
already provides the functional spelling.

The work partitions naturally into a few grouped commits or sub-tasks: 
transforming ({{collectMany}}/{{collectingMany}}/{{flatten}}), reducing 
({{inject}}/{{sum}}/{{countBy}}/{{groupBy}}/{{min}}/{{max}}), slicing 
({{takeWhile}}/{{dropWhile}}/{{split}}), running-fold ({{injectAll}}). Each 
addition follows the conventions established by GROOVY-12054: matching generics 
wildcards, inline javadoc test blocks, and where useful the 
{{BiFunction}}/{{BiPredicate}} + trailing-param variants mirroring 
{{Lambdas.curryWith}}.

h3. Method-ambiguity assessment

*The load-bearing invariant holds:* {{groovy.lang.Closure}} implements none of 
the {{java.util.function}} interfaces, so a Closure argument is never 
assignable to a {{Function}}/{{Predicate}}/{{Consumer}}/{{Comparator}} 
parameter. Any real closure, lambda, or method reference disambiguates by type 
and arity. This is the invariant GROOVY-12054 relied on, and the remaining 
methods are structurally identical.

Verified empirically against {{max}}/{{min}}/{{sort}}, which have carried 
{{Closure}} + {{Comparator}} overloads for years (the exact pattern fat-free 
work creates):

||Call||Dynamic dispatch||@CompileStatic||
|{{max(null)}} (Closure + Comparator)|Resolves _deterministically_ to 
{{Comparator}}, then NPEs — *no ambiguity error*|*Hard compile error*: 
"Reference to method is ambiguous. Cannot choose between \[max(Closure), 
max(Comparator)\]"|
|{{max(s -> s.length())}} — 1-arg lambda|Fine|Fine — arity disambiguates|
|Closure literal (block form)|Fine|Fine — selects the Closure overload|
|Method reference {{String::length}}|Fine|Fine — selects the functional twin|

h3. Residual risks

# *Literal {{null}} argument — the one real case, and narrower than it first 
appears.*
#* Dynamically it is _not_ a crash-ambiguity: Groovy resolves {{foo(null)}} 
deterministically to one overload (for {{max}}/{{min}} it picks 
{{Comparator}}). The hazard is a _silent behaviour change_ — the twin may win 
where the Closure used to — and only ever for an untyped literal {{null}}.
#* The hard ambiguity _error_ appears only under {{@CompileStatic}}. This is 
long-standing Groovy behaviour (reproducible on 4.0.27, predating all fat-free 
work); GROOVY-12054 did not introduce it and this work does not worsen it in 
kind — each twin just extends it to one more method.
#* Concrete regression: methods that today have a _single_ Closure overload 
({{collectMany}}, {{sum}}, {{split}}, {{countBy}}, {{takeWhile}}, 
{{dropWhile}}, {{flatten}}, {{injectAll}}, and the remaining 
{{inject}}/{{groupBy}} forms) would flip a {{@CompileStatic}} {{foo(null)}} 
call from _compiles_ to _compile error_. In practice nobody writes 
{{collectMany(null)}}; GROOVY-12054 proved the blast radius is nil (no call 
site in the tree needed a cast). Treat it as a grep, not a blocker.
# *{{sum}} — semantic (not compile) shift.* {{sum(Iterable, Object 
initialValue)}} already exists; a new {{sum(Iterable, Function)}} is _more 
specific_ than {{Object}}, so real lambdas/method-refs resolve correctly — but 
an object that happens to implement {{Function}} and was being passed as 
{{initialValue}} would silently switch meaning. Low probability.
# *{{min}}/{{max}} — do not add key-extractor {{Function}} twins.* They would 
create the only mixed-SAM site ({{Comparator}} arity-2 + {{Function}} arity-1), 
and are redundant with the existing {{Comparator}} overload + JDK 
{{Comparator.comparing}} — the "JDK already provides it" carve-out. Still add 
the {{Comparator}} twin for the *Map* receiver, which genuinely lacks one.
# *Bare lambda literals under {{@CompileStatic}}* are already settled ({{\{ 
\}}} -> Closure, {{::}}/typed SAM -> twin). Extend {{LambdasTest}} with an STC 
case per new method group so a future STC change cannot regress it silently.

*Bottom line:* no broad ambiguity hazard. The only concrete regression is 
{{@CompileStatic}} + literal {{null}} on single-overload methods, which is 
rare, pre-existing in kind, and grep-checkable.



  was:
GROOVY-12054, GROOVY-12034 and GROOVY-12043 introduced "fat-free" variants of 
common DGM methods accepting {{java.util.function}} types ({{Function}}, 
{{Predicate}}, {{BiFunction}}, ...) alongside the classic {{Closure}} 
overloads, so lambdas and functional values obtained from Java APIs can be used 
with Groovy's collection methods without wrapping. Coverage is currently 
partial, which makes the surface irregular: a user (or an AI coding assistant) 
who learns that {{collect}} accepts a {{Function}} will reasonably expect 
{{collectMany}} to do the same, and currently gets a {{MissingMethodException}}.

This umbrella issue proposes completing the surface to a predictable rule:

{quote}Every Closure-taking collection/map DGM method has a 
functional-interface twin, unless the JDK already provides the 
equivalent.{quote}

Methods currently without any functional variant (Closure-overload count in 
parens): {{collectMany}} (8), {{sum}} (6), {{takeWhile}} (6), {{dropWhile}} 
(6), {{countBy}} (4), {{split}} (5), {{flatten}} (4, the {{flattenUsing}} 
forms), {{injectAll}} (4), {{collectingMany}} (1). Partial coverage to round 
out: {{inject}} (2 of 9), {{groupBy}} (1 of 6), and the key-extractor forms of 
{{min}}/{{max}}.

Deliberate exception to confirm rather than implement: 
{{removeAll}}/{{retainAll}}, where JDK {{Collection#removeIf(Predicate)}} 
already provides the functional spelling.

The work partitions naturally into a few grouped commits or sub-tasks: 
transforming ({{collectMany}}/{{collectingMany}}/{{flatten}}), reducing 
({{inject}}/{{sum}}/{{countBy}}/{{groupBy}}/{{min}}/{{max}}), slicing 
({{takeWhile}}/{{dropWhile}}/{{split}}), running-fold ({{injectAll}}). Each 
addition follows the conventions established by GROOVY-12054: matching generics 
wildcards, inline javadoc test blocks, and where useful the 
{{BiFunction}}/{{BiPredicate}} + trailing-param variants mirroring 
{{Lambdas.curryWith}}.


> Complete the functional-interface (fat-free) variants of Closure-taking DGM 
> methods
> -----------------------------------------------------------------------------------
>
>                 Key: GROOVY-12205
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12205
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Priority: Major
>
> GROOVY-12054, GROOVY-12034 and GROOVY-12043 introduced "fat-free" variants of 
> common DGM methods accepting {{java.util.function}} types ({{Function}}, 
> {{Predicate}}, {{BiFunction}}, ...) alongside the classic {{Closure}} 
> overloads, so lambdas and functional values obtained from Java APIs can be 
> used with Groovy's collection methods without wrapping. Coverage is currently 
> partial, which makes the surface irregular: a user (or an AI coding 
> assistant) who learns that {{collect}} accepts a {{Function}} will reasonably 
> expect {{collectMany}} to do the same, and currently gets a 
> {{MissingMethodException}}.
> This umbrella issue proposes completing the surface to a predictable rule:
> {quote}Every Closure-taking collection/map DGM method has a 
> functional-interface twin, unless the JDK already provides the 
> equivalent.{quote}
> Methods currently without any functional variant (Closure-overload count in 
> parens): {{collectMany}} (8), {{sum}} (6), {{takeWhile}} (6), {{dropWhile}} 
> (6), {{countBy}} (4), {{split}} (5), {{flatten}} (4, the {{flattenUsing}} 
> forms), {{injectAll}} (4), {{collectingMany}} (1). Partial coverage to round 
> out: {{inject}} (2 of 9), {{groupBy}} (1 of 6), and the key-extractor forms 
> of {{min}}/{{max}}.
> Deliberate exception to confirm rather than implement: 
> {{removeAll}}/{{retainAll}}, where JDK {{Collection#removeIf(Predicate)}} 
> already provides the functional spelling.
> The work partitions naturally into a few grouped commits or sub-tasks: 
> transforming ({{collectMany}}/{{collectingMany}}/{{flatten}}), reducing 
> ({{inject}}/{{sum}}/{{countBy}}/{{groupBy}}/{{min}}/{{max}}), slicing 
> ({{takeWhile}}/{{dropWhile}}/{{split}}), running-fold ({{injectAll}}). Each 
> addition follows the conventions established by GROOVY-12054: matching 
> generics wildcards, inline javadoc test blocks, and where useful the 
> {{BiFunction}}/{{BiPredicate}} + trailing-param variants mirroring 
> {{Lambdas.curryWith}}.
> h3. Method-ambiguity assessment
> *The load-bearing invariant holds:* {{groovy.lang.Closure}} implements none 
> of the {{java.util.function}} interfaces, so a Closure argument is never 
> assignable to a {{Function}}/{{Predicate}}/{{Consumer}}/{{Comparator}} 
> parameter. Any real closure, lambda, or method reference disambiguates by 
> type and arity. This is the invariant GROOVY-12054 relied on, and the 
> remaining methods are structurally identical.
> Verified empirically against {{max}}/{{min}}/{{sort}}, which have carried 
> {{Closure}} + {{Comparator}} overloads for years (the exact pattern fat-free 
> work creates):
> ||Call||Dynamic dispatch||@CompileStatic||
> |{{max(null)}} (Closure + Comparator)|Resolves _deterministically_ to 
> {{Comparator}}, then NPEs — *no ambiguity error*|*Hard compile error*: 
> "Reference to method is ambiguous. Cannot choose between \[max(Closure), 
> max(Comparator)\]"|
> |{{max(s -> s.length())}} — 1-arg lambda|Fine|Fine — arity disambiguates|
> |Closure literal (block form)|Fine|Fine — selects the Closure overload|
> |Method reference {{String::length}}|Fine|Fine — selects the functional twin|
> h3. Residual risks
> # *Literal {{null}} argument — the one real case, and narrower than it first 
> appears.*
> #* Dynamically it is _not_ a crash-ambiguity: Groovy resolves {{foo(null)}} 
> deterministically to one overload (for {{max}}/{{min}} it picks 
> {{Comparator}}). The hazard is a _silent behaviour change_ — the twin may win 
> where the Closure used to — and only ever for an untyped literal {{null}}.
> #* The hard ambiguity _error_ appears only under {{@CompileStatic}}. This is 
> long-standing Groovy behaviour (reproducible on 4.0.27, predating all 
> fat-free work); GROOVY-12054 did not introduce it and this work does not 
> worsen it in kind — each twin just extends it to one more method.
> #* Concrete regression: methods that today have a _single_ Closure overload 
> ({{collectMany}}, {{sum}}, {{split}}, {{countBy}}, {{takeWhile}}, 
> {{dropWhile}}, {{flatten}}, {{injectAll}}, and the remaining 
> {{inject}}/{{groupBy}} forms) would flip a {{@CompileStatic}} {{foo(null)}} 
> call from _compiles_ to _compile error_. In practice nobody writes 
> {{collectMany(null)}}; GROOVY-12054 proved the blast radius is nil (no call 
> site in the tree needed a cast). Treat it as a grep, not a blocker.
> # *{{sum}} — semantic (not compile) shift.* {{sum(Iterable, Object 
> initialValue)}} already exists; a new {{sum(Iterable, Function)}} is _more 
> specific_ than {{Object}}, so real lambdas/method-refs resolve correctly — 
> but an object that happens to implement {{Function}} and was being passed as 
> {{initialValue}} would silently switch meaning. Low probability.
> # *{{min}}/{{max}} — do not add key-extractor {{Function}} twins.* They would 
> create the only mixed-SAM site ({{Comparator}} arity-2 + {{Function}} 
> arity-1), and are redundant with the existing {{Comparator}} overload + JDK 
> {{Comparator.comparing}} — the "JDK already provides it" carve-out. Still add 
> the {{Comparator}} twin for the *Map* receiver, which genuinely lacks one.
> # *Bare lambda literals under {{@CompileStatic}}* are already settled ({{\{ 
> \}}} -> Closure, {{::}}/typed SAM -> twin). Extend {{LambdasTest}} with an 
> STC case per new method group so a future STC change cannot regress it 
> silently.
> *Bottom line:* no broad ambiguity hazard. The only concrete regression is 
> {{@CompileStatic}} + literal {{null}} on single-overload methods, which is 
> rare, pre-existing in kind, and grep-checkable.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to