jeudi 11 avril 2019

Determine whether one function "could override" another function

I'm hacking on a mildly esoteric library, and I'm stumped on maybe the most hyperspecific issue ever. I want to check if taking a method reference to some method m could implement some functional interface FI, with or without a receiver parameter (ie. if m belongs to some class C and c instanceof C, I want to check both c::m and C::m).

I've solved this for some cases... check the arity, check the argument types, check the return type, check the exception types, deal with possible receiver parameters, yada yada. But it gets hairy when generics come into play.

interface TypeMatcher {

    boolean isSubtype(final Type type);

    boolean isSupertype(final Type type);

    static TypeMatcher.of(final Type type) {
        // ...
    }
}

class TypeVariableMatcher implements TypeMatcher {

    private final TypeVariable<?> typeVariable;
    private TypeMatcher binding;

    TypeVariableMatcher(final TypeVariable<?> typeVariable) {
        this.typeVariable = typeVariable;
    }

    @Override public boolean isSubtype(final Type type) {
        // TODO: check bounds... how?
        return check(type, TypeMatcher::isSubtype);
    }

    @Override public boolean isSupertype(final Type type) {
        // TODO: check bounds... how?
        return check(type, TypeMatcher::isSupertype);
    }

    private boolean check(final Type type, final BiPredicate<TypeMatcher, Type> callback) {
        if (binding == null) {
            binding = TypeMatcher.of(type);
            return true;
        }
        return callback.test(binding, type);
    }

}

To check if a method could implement the functional method, I just create a TypeMatcher cache (let cache be a Map, then matcherFactory = type -> cache.computeIfAbsent(type, TypeMatcher::of)). Then I just try and match the arguments, return type, and exception types in that order (it's not quite that simple due to varargs and the possible receiver parameter, etc., but you get the jist). The cache helps handle interfaces like java.util.BinaryOperator, but...

As the // TODOs note, this doesn't check bounded type parameters, so this test case would fail:

interface FI<R extends Runnable> {
    void fm(R r1, R r2);
}

interface C {
    void cannotImplementFI(String s1, String s2);
}

...since String would be reported as a "supertype of" R, even though it's not. I'm also not sure how to handle bounded wildcards:

interface FI {
    <T> void fm(Supplier<? extends T> s, Consumer<? super T> c);
}

interface C {
    void canImplementFI(Supplier<String> s, Consumer<Object>);
    void cannotImplementFI(Supplier<Number> s, Consumer<Integer> c);
}

Maybe my approach is wrong, or maybe I've been struggling with the same problem too long and I'm missing something obvious. Any pointers on how to proceed from here would be absolutely lovely!!





Aucun commentaire:

Enregistrer un commentaire