Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/main/java/groovy/transform/Monadic.java
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 src/main/java/org/apache/groovy/runtime/Comprehensions.java
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 &mdash; 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 &mdash; 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);
}
};
}
Comment thread
paulk-asert marked this conversation as resolved.
// General SAM coercion: proxy the closure as the declared interface.
// For pt == Object this returns the Closure unchanged.
return DefaultGroovyMethods.asType(fn, pt);
}
}
Loading
Loading