Paul King created GROOVY-12486:
----------------------------------
Summary: MonadicChecker types the DO generator variable from the
carrier's first type argument
Key: GROOVY-12486
URL: https://issues.apache.org/jira/browse/GROOVY-12486
Project: Groovy
Issue Type: Bug
Components: groovy-typecheckers
Reporter: Paul King
h2. Summary
Under {{MonadicChecker}}, the variable bound by a {{DO}} generator is typed as
the carrier's *first* type argument. For carriers with more than one type
parameter that bind on a later one, the variable gets the wrong type. Examples
are an {{Either<L, R>}} or {{Validation<E, A>}} that is right-biased. The
result is a spurious type error, or code that compiles only by accident.
This affects carriers already on the 6.0.0 allow-list: Vavr {{Either}} and
{{Validation}}, and Functional Java {{Validation}}. It also affects structural
and {{@Monadic}} carriers of the same shape.
h2. Reproducer
{code:groovy}
import groovy.transform.TypeChecked
import java.util.function.Function
import static org.apache.groovy.macrolib.MacroLibGroovyMethods.DO
class Result<E, A> {
final A value
Result(A value) { this.value = value }
static <E, A> Result<E, A> ok(A value) { new Result<E, A>(value) }
def <B> Result<E, B> flatMap(Function<? super A, ? extends Result<E, B>> f)
{ f.apply(value) }
def <B> Result<E, B> map(Function<? super A, ? extends B> f) {
ok(f.apply(value)) }
String toString() { "Ok($value)" }
}
@TypeChecked(extensions = 'groovy.typecheckers.MonadicChecker')
def f() {
DO(x in Result.<String, Integer>ok(1)) {
int n = x
Result.<String, Integer>ok(n + 1)
}
}
println f()
{code}
h2. Actual (6.0.0)
{noformat}
[Static type checking] - Cannot assign value of type java.lang.String to
variable of type int
{noformat}
{{x}} is typed as {{E}} ({{String}}). It should be {{A}} ({{Integer}}), the
parameter type of {{flatMap}}'s function argument.
The same happens with a real right-biased {{Either}}. With an uninferred left
type, e.g. {{DO(x in Either.right(1)) \{ Either.right(x + 1) \}}}, the error is:
{noformat}
[Static type checking] - Cannot find matching method #L#plus(int)
{noformat}
When the type arguments are explicit, code such as {{x + 1}} still compiles,
because it resolves to {{String.plus}}. It then works at runtime only because
the actual value is an {{Integer}}.
h2. Expected
{{x}} is typed as {{Integer}}, the reproducer compiles, and it prints {{Ok(2)}}.
h2. Cause and suggested fix
{{MonadicChecker.elementType}} returns {{carrier.genericsTypes[0]}}. That is
right for single-parameter carriers ({{Optional}}, {{Stream}},
{{CompletionStage}}, {{Awaitable}}) but not in general.
The element type should come from the carrier's bind method. Find the
single-argument method named by the carrier's bind entry ({{flatMap}}, or
{{bind}} for Functional Java, or the {{@Monadic}} value). Take the first type
argument of its function parameter ({{? super A}}) and resolve it against the
receiver's type arguments. Fall back to the last type argument, then
{{Object}}, when that can't be determined, e.g. for a raw receiver.
h2. Tests
* A two-parameter structural carrier, as in the reproducer.
* A {{@Monadic}} carrier whose type parameters are declared in a different
order from the bound one.
* A name-keyed carrier, using a test stub in the style of
{{fj/data/Option.groovy}}, e.g. a {{Validation<E, A>}} stub.
* Regression check that single-parameter carriers ({{Optional}}, {{Stream}},
{{CompletableFuture}}) keep their current typing.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)