This is an automated email from the ASF dual-hosted git repository.

paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new 3754ac857c GROOVY-12043: Provide curryWith helpers
3754ac857c is described below

commit 3754ac857ce9e2e24821ae8f800f8aed55028711
Author: Paul King <[email protected]>
AuthorDate: Fri May 29 14:14:53 2026 +1000

    GROOVY-12043: Provide curryWith helpers
---
 src/main/java/org/apache/groovy/util/Closures.java | 239 +++++++++++++++++++++
 src/main/java/org/apache/groovy/util/Lambdas.java  |  83 +++++++
 src/spec/doc/core-closures.adoc                    |  55 +++++
 .../org/apache/groovy/util/ClosuresTest.groovy     | 212 ++++++++++++++++++
 .../org/apache/groovy/util/LambdasTest.groovy      | 123 +++++++++++
 5 files changed, 712 insertions(+)

diff --git a/src/main/java/org/apache/groovy/util/Closures.java 
b/src/main/java/org/apache/groovy/util/Closures.java
new file mode 100644
index 0000000000..d27123e665
--- /dev/null
+++ b/src/main/java/org/apache/groovy/util/Closures.java
@@ -0,0 +1,239 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.util;
+
+import groovy.lang.Closure;
+
+import java.io.Serial;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * Helpers that return hybrid one-argument functions which are both
+ * {@link Closure} and the appropriate {@link java.util.function} SAM
+ * ({@link Predicate}, {@link Function} or {@link Consumer}).
+ * <p>
+ * For variants whose results are only the SAM type, see {@link Lambdas}.
+ *
+ * @since 6.0.0
+ */
+public class Closures {
+
+    private Closures() {}
+
+    /**
+     * Lifts a {@link Predicate} into a hybrid that is also a
+     * {@link Closure}. If {@code p} is already a {@link PredicateClosure}
+     * it is returned unchanged.
+     * <pre class="language-groovy">
+     * Predicate&lt;Integer&gt; isEven = n -&gt; n % 2 == 0
+     * assert [1, 2, 3, 4].findAll(Closures.from(isEven)) == [2, 4]
+     * </pre>
+     *
+     * @since 6.0.0
+     */
+    public static <T> PredicateClosure<T> from(Predicate<T> p) {
+        if (p instanceof PredicateClosure) {
+            @SuppressWarnings("unchecked")
+            PredicateClosure<T> pc = (PredicateClosure<T>) p;
+            return pc;
+        }
+        return new PredicateClosure<>(p);
+    }
+
+    /**
+     * Lifts a {@link Function} into a hybrid that is also a
+     * {@link Closure}. If {@code f} is already a {@link FunctionClosure}
+     * it is returned unchanged.
+     *
+     * @since 6.0.0
+     */
+    public static <T, R> FunctionClosure<T, R> from(Function<T, R> f) {
+        if (f instanceof FunctionClosure) {
+            @SuppressWarnings("unchecked")
+            FunctionClosure<T, R> fc = (FunctionClosure<T, R>) f;
+            return fc;
+        }
+        return new FunctionClosure<>(f);
+    }
+
+    /**
+     * Lifts a {@link Consumer} into a hybrid that is also a
+     * {@link Closure}. If {@code c} is already a {@link ConsumerClosure}
+     * it is returned unchanged.
+     *
+     * @since 6.0.0
+     */
+    public static <T> ConsumerClosure<T> from(Consumer<T> c) {
+        if (c instanceof ConsumerClosure) {
+            @SuppressWarnings("unchecked")
+            ConsumerClosure<T> cc = (ConsumerClosure<T>) c;
+            return cc;
+        }
+        return new ConsumerClosure<>(c);
+    }
+
+    /**
+     * Right-partials a {@link BiPredicate} and lifts the result into a
+     * hybrid usable as both {@link Closure} and {@link Predicate}.
+     * Equivalent to {@code from(Lambdas.curryWith(bp, p))}.
+     * <pre class="language-groovy">
+     * BiPredicate&lt;Integer,Integer&gt; divisibleBy = (n, d) -&gt; n % d == 0
+     * assert [1, 2, 3, 4, 5].findAll(curryWith(divisibleBy, 2)) == [2, 4]
+     * </pre>
+     *
+     * @since 6.0.0
+     */
+    public static <T, P> PredicateClosure<T> curryWith(BiPredicate<? super T, 
? super P> bp, P p) {
+        return from(Lambdas.curryWith(bp, p));
+    }
+
+    /**
+     * Right-partials a {@link BiFunction} and lifts the result into a
+     * hybrid usable as both {@link Closure} and {@link Function}.
+     * Equivalent to {@code from(Lambdas.curryWith(bf, p))}.
+     *
+     * @since 6.0.0
+     */
+    public static <T, P, R> FunctionClosure<T, R> curryWith(BiFunction<? super 
T, ? super P, ? extends R> bf, P p) {
+        return from(Lambdas.curryWith(bf, p));
+    }
+
+    /**
+     * Right-partials a {@link BiConsumer} and lifts the result into a
+     * hybrid usable as both {@link Closure} and {@link Consumer}.
+     * Equivalent to {@code from(Lambdas.curryWith(bc, p))}.
+     *
+     * @since 6.0.0
+     */
+    public static <T, P> ConsumerClosure<T> curryWith(BiConsumer<? super T, ? 
super P> bc, P p) {
+        return from(Lambdas.curryWith(bc, p));
+    }
+
+    /**
+     * Hybrid one-argument function that is both a {@link Closure} and a
+     * {@link Predicate}.
+     *
+     * @since 6.0.0
+     */
+    public static class PredicateClosure<T> extends Closure<Boolean> 
implements Predicate<T> {
+        @Serial private static final long serialVersionUID = 1L;
+        private final Predicate<T> delegate;
+
+        PredicateClosure(Predicate<T> delegate) {
+            super(null, null);
+            this.delegate = delegate;
+            this.maximumNumberOfParameters = 1;
+            this.parameterTypes = new Class<?>[]{Object.class};
+        }
+
+        @Override
+        public boolean test(T t) {
+            return delegate.test(t);
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public Boolean call(Object arg) {
+            return test((T) arg);
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public Boolean call(Object... args) {
+            return test((T) args[0]);
+        }
+    }
+
+    /**
+     * Hybrid one-argument function that is both a {@link Closure} and a
+     * {@link Function}.
+     *
+     * @since 6.0.0
+     */
+    public static class FunctionClosure<T, R> extends Closure<R> implements 
Function<T, R> {
+        @Serial private static final long serialVersionUID = 1L;
+        private final Function<T, R> delegate;
+
+        FunctionClosure(Function<T, R> delegate) {
+            super(null, null);
+            this.delegate = delegate;
+            this.maximumNumberOfParameters = 1;
+            this.parameterTypes = new Class<?>[]{Object.class};
+        }
+
+        @Override
+        public R apply(T t) {
+            return delegate.apply(t);
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public R call(Object arg) {
+            return apply((T) arg);
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public R call(Object... args) {
+            return apply((T) args[0]);
+        }
+    }
+
+    /**
+     * Hybrid one-argument function that is both a {@link Closure} and a
+     * {@link Consumer}.
+     *
+     * @since 6.0.0
+     */
+    public static class ConsumerClosure<T> extends Closure<Void> implements 
Consumer<T> {
+        @Serial private static final long serialVersionUID = 1L;
+        private final Consumer<T> delegate;
+
+        ConsumerClosure(Consumer<T> delegate) {
+            super(null, null);
+            this.delegate = delegate;
+            this.maximumNumberOfParameters = 1;
+            this.parameterTypes = new Class<?>[]{Object.class};
+        }
+
+        @Override
+        public void accept(T t) {
+            delegate.accept(t);
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public Void call(Object arg) {
+            accept((T) arg);
+            return null;
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public Void call(Object... args) {
+            accept((T) args[0]);
+            return null;
+        }
+    }
+}
diff --git a/src/main/java/org/apache/groovy/util/Lambdas.java 
b/src/main/java/org/apache/groovy/util/Lambdas.java
new file mode 100644
index 0000000000..3a9b3a4bb0
--- /dev/null
+++ b/src/main/java/org/apache/groovy/util/Lambdas.java
@@ -0,0 +1,83 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.util;
+
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * Helpers that return {@link java.util.function} types.
+ * <p>
+ * For variants whose results are also {@link groovy.lang.Closure}
+ * instances, see {@link Closures}.
+ *
+ * @since 6.0.0
+ */
+public class Lambdas {
+
+    private Lambdas() {}
+
+    /**
+     * Right-partials a {@link BiPredicate} with the supplied parameter,
+     * returning a {@link Predicate} that fixes the second argument.
+     * <pre class="language-groovy">
+     * BiPredicate&lt;Integer,Integer&gt; divisibleBy = (n, d) -&gt; n % d == 0
+     * assert [1, 2, 3, 4, 5].stream().filter(curryWith(divisibleBy, 
2)).toList() == [2, 4]
+     * </pre>
+     *
+     * @since 6.0.0
+     */
+    public static <T, P> Predicate<T> curryWith(BiPredicate<? super T, ? super 
P> bp, P p) {
+        return t -> bp.test(t, p);
+    }
+
+    /**
+     * Right-partials a {@link BiFunction} with the supplied parameter,
+     * returning a {@link Function} that fixes the second argument.
+     * <pre class="language-groovy">
+     * BiFunction&lt;String,Integer,String&gt; repeat = (s, n) -&gt; s * n
+     * assert ['a', 'b', 'c'].stream().map(curryWith(repeat, 3)).toList() == 
['aaa', 'bbb', 'ccc']
+     * </pre>
+     *
+     * @since 6.0.0
+     */
+    public static <T, P, R> Function<T, R> curryWith(BiFunction<? super T, ? 
super P, ? extends R> bf, P p) {
+        return t -> bf.apply(t, p);
+    }
+
+    /**
+     * Right-partials a {@link BiConsumer} with the supplied parameter,
+     * returning a {@link Consumer} that fixes the second argument.
+     * <pre class="language-groovy">
+     * def sink = []
+     * BiConsumer&lt;String,List&gt; addTo = (s, list) -&gt; list &lt;&lt; s
+     * ['a', 'b', 'c'].stream().forEach(curryWith(addTo, sink))
+     * assert sink == ['a', 'b', 'c']
+     * </pre>
+     *
+     * @since 6.0.0
+     */
+    public static <T, P> Consumer<T> curryWith(BiConsumer<? super T, ? super 
P> bc, P p) {
+        return t -> bc.accept(t, p);
+    }
+}
diff --git a/src/spec/doc/core-closures.adoc b/src/spec/doc/core-closures.adoc
index f9e945bb8f..53c67222c5 100644
--- a/src/spec/doc/core-closures.adoc
+++ b/src/spec/doc/core-closures.adoc
@@ -520,6 +520,61 @@ 
include::../test/ClosuresSpecTest.groovy[tags=ncurry,indent=0]
 <4> it is also possible to set multiple parameters, starting from the 
specified index
 <5> the resulting function accepts as many parameters as the initial one minus 
the number of parameters set by `ncurry`
 
+==== Currying functional interfaces
+
+When working with `java.util.function` types — for example `BiPredicate`,
+`BiFunction` or `BiConsumer` — `Closure.rcurry` is not applicable. Two
+sibling utility classes provide right-partial helpers, differing only in
+the type returned:
+
+* `org.apache.groovy.util.Lambdas#curryWith` returns a bare
+  `Predicate`/`Function`/`Consumer`. Use when the result will flow into
+  JDK APIs (e.g. `Stream.filter`, `Optional.map`) or into DGM methods
+  that already accept SAM types (e.g. `partitionPoint`, `takeIf`,
+  `groupConsecutive`).
+* `org.apache.groovy.util.Closures#curryWith` returns a hybrid value
+  that is both the SAM type *and* a `Closure`. Use when the result
+  should also be usable with DGM methods that accept a `Closure`
+  (e.g. `findAll`, `collect`, `each`, `find`, `any`, `every`).
+
+[source,groovy]
+----
+import static org.apache.groovy.util.Lambdas.curryWith as curryWithSam
+import static org.apache.groovy.util.Closures.curryWith as curryWithClosure
+import java.util.function.BiPredicate
+
+BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0
+
+// Lambdas.curryWith: flows into JDK streams / SAM-accepting DGM
+assert [1, 2, 3, 4, 5, 6].stream().filter(curryWithSam(divisibleBy, 
2)).toList() == [2, 4, 6]
+
+// Closures.curryWith: flows into Closure-accepting DGM as well
+assert [1, 2, 3, 4, 5, 6].findAll(curryWithClosure(divisibleBy, 2)) == [2, 4, 
6]
+----
+
+In both forms a single allocation is paid per `curryWith` call rather
+than per element of the traversal.
+
+`Closures.from(Predicate)` (and the `Function`/`Consumer` overloads)
+lifts an existing single-argument SAM into the same hybrid form, useful
+when an external API hands you a `Predicate` you want to feed into a
+`Closure`-accepting DGM method:
+
+[source,groovy]
+----
+import static org.apache.groovy.util.Closures.from
+import java.util.function.Predicate
+
+Predicate<Integer> isEven = n -> n % 2 == 0
+assert [1, 2, 3, 4].findAll(from(isEven)) == [2, 4]
+----
+
+Three overloads are provided:
+
+* `curryWith(BiPredicate<T,P>, P) -> Predicate<T>`
+* `curryWith(BiFunction<T,P,R>, P) -> Function<T,R>`
+* `curryWith(BiConsumer<T,P>, P) -> Consumer<T>`
+
 === Memoization
 
 Memoization allows the result of the call of a closure to be cached. It is 
interesting if the computation done by a
diff --git a/src/test/groovy/org/apache/groovy/util/ClosuresTest.groovy 
b/src/test/groovy/org/apache/groovy/util/ClosuresTest.groovy
new file mode 100644
index 0000000000..29964a752e
--- /dev/null
+++ b/src/test/groovy/org/apache/groovy/util/ClosuresTest.groovy
@@ -0,0 +1,212 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.util
+
+import groovy.lang.Closure
+import groovy.transform.CompileStatic
+import org.junit.jupiter.api.Test
+
+import java.util.function.BiConsumer
+import java.util.function.BiFunction
+import java.util.function.BiPredicate
+import java.util.function.Consumer
+import java.util.function.Function
+import java.util.function.Predicate
+import java.util.stream.Stream
+
+import static org.apache.groovy.util.Closures.curryWith
+import static org.apache.groovy.util.Closures.from
+
+class ClosuresTest {
+
+    // ---- from(Predicate/Function/Consumer) -----------------------------
+
+    @Test
+    void fromLiftsPredicateIntoHybrid() {
+        Predicate<Integer> isEven = n -> n % 2 == 0
+        def lifted = from(isEven)
+
+        assert lifted instanceof Predicate
+        assert lifted instanceof Closure
+        assert lifted.test(4)
+        assert !lifted.test(5)
+    }
+
+    @Test
+    void fromLiftsFunctionIntoHybrid() {
+        Function<Integer, Integer> twice = n -> n * 2
+        def lifted = from(twice)
+
+        assert lifted instanceof Function
+        assert lifted instanceof Closure
+        assert lifted.apply(3) == 6
+    }
+
+    @Test
+    void fromLiftsConsumerIntoHybrid() {
+        List<String> sink = []
+        Consumer<String> intoSink = s -> sink << s
+        def lifted = from(intoSink)
+
+        assert lifted instanceof Consumer
+        assert lifted instanceof Closure
+        lifted.accept('a')
+        assert sink == ['a']
+    }
+
+    @Test
+    void fromIsIdempotentForAlreadyHybrid() {
+        Predicate<Integer> isEven = n -> n % 2 == 0
+        def first = from(isEven)
+        def second = from(first)
+
+        assert second.is(first)
+    }
+
+    // ---- from feeds DGM (Closure-accepting) ---------------------------
+
+    @Test
+    void fromFeedsDgmFindAll() {
+        Predicate<Integer> isEven = n -> n % 2 == 0
+        assert [1, 2, 3, 4, 5].findAll(from(isEven)) == [2, 4]
+    }
+
+    @Test
+    void fromFeedsDgmCollect() {
+        Function<Integer, Integer> twice = n -> n * 2
+        assert [1, 2, 3].collect(from(twice)) == [2, 4, 6]
+    }
+
+    @Test
+    void fromFeedsDgmEach() {
+        List<String> sink = []
+        Consumer<String> intoSink = s -> sink << s
+        ['a', 'b', 'c'].each(from(intoSink))
+        assert sink == ['a', 'b', 'c']
+    }
+
+    // ---- from feeds JDK stream/SAM APIs --------------------------------
+
+    @Test
+    void fromFeedsStreamFilter() {
+        Predicate<Integer> isEven = n -> n % 2 == 0
+        assert Stream.of(1, 2, 3, 4, 5).filter(from(isEven)).toList() == [2, 4]
+    }
+
+    // ---- curryWith composes from + Lambdas.curryWith -------------------
+
+    @Test
+    void curryWithBiPredicateReturnsHybrid() {
+        BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0
+        def result = curryWith(divisibleBy, 2)
+
+        assert result instanceof Predicate
+        assert result instanceof Closure
+        assert result.test(4)
+        assert !result.test(5)
+    }
+
+    @Test
+    void curryWithBiFunctionReturnsHybrid() {
+        BiFunction<String, Integer, String> repeat = (s, n) -> s * n
+        def result = curryWith(repeat, 3)
+
+        assert result instanceof Function
+        assert result instanceof Closure
+        assert result.apply('a') == 'aaa'
+    }
+
+    @Test
+    void curryWithBiConsumerReturnsHybrid() {
+        List<String> sink = []
+        BiConsumer<String, List<String>> addTo = (s, list) -> list << s
+        def result = curryWith(addTo, sink)
+
+        assert result instanceof Consumer
+        assert result instanceof Closure
+        result.accept('a')
+        assert sink == ['a']
+    }
+
+    // ---- curryWith feeds DGM ------------------------------------------
+
+    @Test
+    void curryWithFeedsDgmFindAll() {
+        BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0
+
+        assert [1, 2, 3, 4, 5, 6].findAll(curryWith(divisibleBy, 2)) == [2, 4, 
6]
+        assert [1, 2, 3, 4, 5, 6].findAll(curryWith(divisibleBy, 3)) == [3, 6]
+    }
+
+    @Test
+    void curryWithFeedsDgmCollect() {
+        BiFunction<String, Integer, String> repeat = (s, n) -> s * n
+
+        assert ['a', 'b', 'c'].collect(curryWith(repeat, 3)) == ['aaa', 'bbb', 
'ccc']
+    }
+
+    @Test
+    void curryWithFeedsDgmEach() {
+        List<String> sink = []
+        BiConsumer<String, List<String>> addTo = (s, list) -> list << s
+
+        ['a', 'b', 'c'].each(curryWith(addTo, sink))
+        assert sink == ['a', 'b', 'c']
+    }
+
+    // ---- curryWith feeds JDK streams ----------------------------------
+
+    @Test
+    void curryWithFeedsStreamFilter() {
+        BiPredicate<Integer, Integer> greaterThan = (n, threshold) -> n > 
threshold
+
+        List<Integer> result = Stream.of(1, 2, 3, 4, 5)
+                .filter(curryWith(greaterThan, 2))
+                .toList()
+
+        assert result == [3, 4, 5]
+    }
+
+    // ---- @CompileStatic --------------------------------------------------
+
+    @Test
+    void curryWithUnderCompileStatic() {
+        assert CompileStaticUsage.evensViaFindAll([1, 2, 3, 4, 5, 6]) == [2, 
4, 6]
+        assert CompileStaticUsage.evensViaStream([1, 2, 3, 4, 5, 6]) == [2, 4, 
6]
+        assert CompileStaticUsage.doublesViaCollect([1, 2, 3, 4]) == [false, 
true, false, true]
+    }
+
+    @CompileStatic
+    static class CompileStaticUsage {
+        static List<Integer> evensViaFindAll(List<Integer> input) {
+            BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0
+            input.findAll(curryWith(divisibleBy, 2))
+        }
+
+        static List<Integer> evensViaStream(List<Integer> input) {
+            BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0
+            input.stream().filter(curryWith(divisibleBy, 2)).toList()
+        }
+
+        static List<Boolean> doublesViaCollect(List<Integer> input) {
+            BiFunction<Integer, Integer, Boolean> divisibleBy = (n, d) -> n % 
d == 0
+            input.collect(curryWith(divisibleBy, 2))
+        }
+    }
+}
diff --git a/src/test/groovy/org/apache/groovy/util/LambdasTest.groovy 
b/src/test/groovy/org/apache/groovy/util/LambdasTest.groovy
new file mode 100644
index 0000000000..8a5ffa8457
--- /dev/null
+++ b/src/test/groovy/org/apache/groovy/util/LambdasTest.groovy
@@ -0,0 +1,123 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.util
+
+import groovy.lang.Closure
+import groovy.transform.CompileStatic
+import org.junit.jupiter.api.Test
+
+import java.util.function.BiConsumer
+import java.util.function.BiFunction
+import java.util.function.BiPredicate
+import java.util.function.Consumer
+import java.util.function.Function
+import java.util.function.Predicate
+import java.util.stream.Stream
+
+import static org.apache.groovy.util.Lambdas.curryWith
+
+class LambdasTest {
+
+    @Test
+    void curryWithBiPredicate() {
+        BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0
+        Predicate<Integer> isEven = curryWith(divisibleBy, 2)
+
+        assert isEven.test(4)
+        assert !isEven.test(5)
+    }
+
+    @Test
+    void curryWithBiFunction() {
+        BiFunction<String, Integer, String> repeat = (s, n) -> s * n
+        Function<String, String> triple = curryWith(repeat, 3)
+
+        assert triple.apply('a') == 'aaa'
+        assert triple.apply('xy') == 'xyxyxy'
+    }
+
+    @Test
+    void curryWithBiConsumer() {
+        List<String> sink = []
+        BiConsumer<String, List<String>> addTo = (s, list) -> list << s
+        Consumer<String> intoSink = curryWith(addTo, sink)
+
+        intoSink.accept('a')
+        intoSink.accept('b')
+        assert sink == ['a', 'b']
+    }
+
+    @Test
+    void resultIsBareSamNotClosure() {
+        BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0
+        def result = curryWith(divisibleBy, 2)
+
+        assert result instanceof Predicate
+        assert !(result instanceof Closure)
+    }
+
+    @Test
+    void curryWithFeedsStreamFilter() {
+        BiPredicate<Integer, Integer> greaterThan = (n, threshold) -> n > 
threshold
+
+        List<Integer> result = Stream.of(1, 2, 3, 4, 5)
+                .filter(curryWith(greaterThan, 2))
+                .toList()
+
+        assert result == [3, 4, 5]
+    }
+
+    @Test
+    void curryWithFeedsSamAcceptingDgm() {
+        // partitionPoint(List, Predicate) is SAM-accepting DGM
+        BiPredicate<Integer, Integer> lessThan = (n, threshold) -> n < 
threshold
+
+        assert [1, 2, 3, 4, 5, 6].partitionPoint(curryWith(lessThan, 4)) == 3
+    }
+
+    @Test
+    void curryWithReturnsFreshFunctionsThatShareNoState() {
+        BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0
+        Predicate<Integer> isEven = curryWith(divisibleBy, 2)
+        Predicate<Integer> isMultipleOf3 = curryWith(divisibleBy, 3)
+
+        assert isEven.test(6) && isMultipleOf3.test(6)
+        assert isEven.test(4) && !isMultipleOf3.test(4)
+        assert !isEven.test(9) && isMultipleOf3.test(9)
+    }
+
+    @Test
+    void curryWithUnderCompileStatic() {
+        assert CompileStaticUsage.evensViaStream([1, 2, 3, 4, 5, 6]) == [2, 4, 
6]
+        assert CompileStaticUsage.tripledViaStream(['a', 'b']) == ['aaa', 
'bbb']
+    }
+
+    @CompileStatic
+    static class CompileStaticUsage {
+        static List<Integer> evensViaStream(List<Integer> input) {
+            BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0
+            input.stream().filter(curryWith(divisibleBy, 2)).toList()
+        }
+
+        static List<String> tripledViaStream(List<String> input) {
+            BiFunction<String, Integer, String> repeat = (s, n) -> s * n
+            input.stream().map(curryWith(repeat, 3)).toList()
+        }
+    }
+}

Reply via email to