-
Notifications
You must be signed in to change notification settings - Fork 1.9k
GROOVY-12021: Add DO macro for monadic comprehensions over Optional/S… #2545
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| /* | ||
| * 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 groovy.transform; | ||
|
|
||
| import org.apache.groovy.lang.annotation.Incubating; | ||
|
|
||
| import java.lang.annotation.Documented; | ||
| import java.lang.annotation.ElementType; | ||
| import java.lang.annotation.Retention; | ||
| import java.lang.annotation.RetentionPolicy; | ||
| import java.lang.annotation.Target; | ||
|
|
||
| /** | ||
| * Marks a type as participating in monadic comprehensions (the {@code DO} macro). | ||
| * Optional members declare the bind/map method names when they diverge from the | ||
| * structural convention ({@code flatMap}/{@code map}). When both are omitted, the | ||
| * annotation merely opts the type in and the structural defaults apply. | ||
| * <p> | ||
| * Modelled on {@link groovy.transform.Reducer}: a pure marker, read by tooling, | ||
| * with no AST transformation. The runtime dispatcher and the type checker match | ||
| * this annotation <em>by simple name</em> ({@code Monadic}), exactly as | ||
| * {@code groovy.typecheckers.CombinerChecker} matches {@code @Reducer}/{@code @Associative}. | ||
| * | ||
| * @since 6.0.0 | ||
| */ | ||
| @Documented | ||
| @Incubating | ||
| @Target(ElementType.TYPE) | ||
| @Retention(RetentionPolicy.RUNTIME) | ||
| public @interface Monadic { | ||
| /** The flatMap-shaped bind method name. Empty means the structural default {@code flatMap}. */ | ||
| String bind() default ""; | ||
|
|
||
| /** The map method name. Empty means the structural default {@code map}. */ | ||
| String map() default ""; | ||
| } |
190 changes: 190 additions & 0 deletions
190
src/main/java/org/apache/groovy/runtime/Comprehensions.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| /* | ||
| * 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.runtime; | ||
|
|
||
| import groovy.lang.Closure; | ||
| import org.apache.groovy.lang.annotation.Incubating; | ||
| import org.codehaus.groovy.runtime.DefaultGroovyMethods; | ||
| import org.codehaus.groovy.runtime.InvokerHelper; | ||
|
|
||
| import java.lang.annotation.Annotation; | ||
| import java.lang.reflect.Method; | ||
| import java.util.function.Function; | ||
|
|
||
| /** | ||
| * Runtime bind/map dispatcher for monadic comprehensions — the emit target | ||
| * of the {@code DO} macro. | ||
| * <p> | ||
| * The macro runs at {@code SEMANTIC_ANALYSIS}, <em>before</em> type checking, so it | ||
| * cannot know the carrier's bind-method name and cannot emit it directly. Instead it | ||
| * emits {@code Comprehensions.bind(carrier) { x -> ... }} calls; this class resolves | ||
| * the carrier-specific method at runtime (dynamic Groovy), while the | ||
| * {@code groovy.typecheckers.MonadicChecker} type-checking extension specialises this | ||
| * one signature under {@code @CompileStatic}. | ||
| * <p> | ||
| * This is runtime support invoked from generated bytecode, hence its placement in | ||
| * core alongside the rest of the Groovy runtime; the {@code DO} macro and the type | ||
| * checker are compile-time only and remain in their optional modules. | ||
| * <p> | ||
| * Participation is resolved first-match-wins: | ||
| * <ol> | ||
| * <li>standard allow-list ({@link MonadicCarrierRegistry});</li> | ||
| * <li>structural ({@code flatMap}/{@code map} present);</li> | ||
| * <li>{@code @Monadic} opt-in (matched by simple name; honours {@code bind}/{@code map} overrides).</li> | ||
| * </ol> | ||
| * A configured marker interface is a further opt-in mechanism that is not yet | ||
| * implemented. | ||
| * <p> | ||
| * Surface generosity: the carrier's bind method may accept a {@link Function} or a | ||
| * {@code Closure}; the closure is adapted to whichever the target declares. Monad | ||
| * laws are not enforced — structural participation, algebraic-law obligation | ||
| * on the implementer (the {@code @Reducer}/{@code @Associative} treatment). | ||
| * | ||
| * @since 6.0.0 | ||
| */ | ||
| @Incubating | ||
| public final class Comprehensions { | ||
|
|
||
| private Comprehensions() {} | ||
|
|
||
| /** Bind (flatMap-shaped): {@code carrier.<bind>(x -> fn(x))} where {@code fn} yields the same carrier. */ | ||
| public static Object bind(Object carrier, Closure<?> fn) { | ||
| return dispatch(carrier, fn, true); | ||
| } | ||
|
|
||
| /** Map: {@code carrier.<map>(x -> fn(x))} where {@code fn} yields a plain value. */ | ||
| public static Object map(Object carrier, Closure<?> fn) { | ||
| return dispatch(carrier, fn, false); | ||
| } | ||
|
|
||
| private static Object dispatch(Object carrier, Closure<?> fn, boolean bindRole) { | ||
| if (carrier == null) { | ||
| throw new IllegalArgumentException( | ||
| "Monadic comprehension carrier is null; null cannot participate as a carrier"); | ||
| } | ||
| Class<?> type = carrier.getClass(); | ||
| String method = resolveMethodName(carrier, type, bindRole); | ||
| if (method == null) { | ||
| String role = bindRole ? "bind (flatMap-shaped)" : "map"; | ||
| String structural = bindRole ? "flatMap" : "map"; | ||
| throw new IllegalArgumentException( | ||
| "Type " + type.getName() + " does not participate in monadic comprehensions: " | ||
| + "no " + role + " method found (not in the standard carrier allow-list, " | ||
| + "has no structural '" + structural + "' method, and is not annotated @Monadic)"); | ||
| } | ||
| Object arg = adaptClosure(type, method, fn); | ||
| return InvokerHelper.invokeMethod(carrier, method, arg); | ||
| } | ||
|
|
||
| private static String resolveMethodName(Object carrier, Class<?> type, boolean bindRole) { | ||
| // 1. standard allow-list (Class- or name-keyed) | ||
| String[] bindMap = MonadicCarrierRegistry.lookupBindMap(carrier); | ||
| if (bindMap != null) { | ||
| return bindRole ? bindMap[0] : bindMap[1]; | ||
| } | ||
| // 2. structural | ||
| String structural = bindRole ? "flatMap" : "map"; | ||
| if (findSingleArgMethod(type, structural) != null) { | ||
| return structural; | ||
| } | ||
| // 3. @Monadic opt-in (matched by simple name, like @Reducer/@Associative) | ||
| String monadic = monadicMethodName(type, bindRole); | ||
| if (monadic != null && findSingleArgMethod(type, monadic) != null) { | ||
| return monadic; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| private static String monadicMethodName(Class<?> type, boolean bindRole) { | ||
| for (Class<?> c = type; c != null && c != Object.class; c = c.getSuperclass()) { | ||
| String n = readMonadicMember(c.getDeclaredAnnotations(), bindRole); | ||
| if (n != null) return n; | ||
| for (Class<?> i : c.getInterfaces()) { | ||
| String ni = readMonadicMember(i.getDeclaredAnnotations(), bindRole); | ||
| if (ni != null) return ni; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| private static String readMonadicMember(Annotation[] annotations, boolean bindRole) { | ||
| for (Annotation a : annotations) { | ||
| if (!"Monadic".equals(a.annotationType().getSimpleName())) continue; | ||
| String configured = invokeStringMember(a, bindRole ? "bind" : "map"); | ||
| if (configured == null || configured.isEmpty()) { | ||
| return bindRole ? "flatMap" : "map"; // opted in, structural defaults | ||
| } | ||
| return configured; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| private static String invokeStringMember(Annotation a, String member) { | ||
| try { | ||
| Object v = a.annotationType().getMethod(member).invoke(a); | ||
| return v == null ? null : v.toString(); | ||
| } catch (ReflectiveOperationException ignored) { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| private static Method findSingleArgMethod(Class<?> type, String name) { | ||
| for (Method m : type.getMethods()) { | ||
| if (m.getParameterCount() == 1 && m.getName().equals(name) | ||
| && !m.isBridge() && !m.isSynthetic()) { | ||
| return m; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Adapt the generator closure to whatever single-arg type the carrier's bind | ||
| * method declares: | ||
| * <ul> | ||
| * <li>a {@code Closure}-typed parameter receives the closure directly;</li> | ||
| * <li>a {@link Function}-typed parameter receives a thin wrapper;</li> | ||
| * <li>any other functional interface (for example Functional Java's | ||
| * {@code fj.F}) receives the closure coerced to that interface.</li> | ||
| * </ul> | ||
| */ | ||
| private static Object adaptClosure(Class<?> type, String method, final Closure<?> fn) { | ||
| Method m = findSingleArgMethod(type, method); | ||
| Class<?> pt = (m != null) ? m.getParameterTypes()[0] : null; | ||
| if (pt == null || Closure.class.isAssignableFrom(pt)) { | ||
| return fn; | ||
| } | ||
| if (pt == Function.class) { | ||
| // Exact Function (not a supertype): Object is a supertype too, and a | ||
| // structural Groovy carrier declaring 'flatMap(c)' lands with pt=Object; | ||
| // the user's body typically calls c.call(x), which would fail against | ||
| // a Function wrapper. Untyped Object falls through to the asType | ||
| // branch below, which returns the Closure unchanged. | ||
| return new Function<Object, Object>() { | ||
| @Override | ||
| public Object apply(Object value) { | ||
| return fn.call(value); | ||
| } | ||
| }; | ||
| } | ||
| // General SAM coercion: proxy the closure as the declared interface. | ||
| // For pt == Object this returns the Closure unchanged. | ||
| return DefaultGroovyMethods.asType(fn, pt); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.