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<Integer> isEven = n -> 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<Integer,Integer> divisibleBy = (n, d) -> 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<Integer,Integer> divisibleBy = (n, d) -> 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<String,Integer,String> repeat = (s, n) -> 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<String,List> addTo = (s, list) -> list << 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()
+ }
+ }
+}