Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pass a parameter to generic function with several upper bounds

Can't pass a parameter to Combiner().combine() function.

Android Studio can't recognize that arg extends Foo and implements Bar. What am I doing wrong?

abstract class Foo {
    val f: Int = 1
}

interface Bar {
    val b: String get() = "a"
}

class Combiner {
    fun <T> combine(arg: T): Pair<Int, String> where T : Foo, T : Bar {
        return arg.f to arg.b
    }
}

class Program {
    fun main() {
        val list: List<Foo> = arrayListOf()
        list.forEach {
            if (it is Bar) {
                Combiner().combine(it) //inferred type Any is not a subtype of Foo
            }
        }
    }
}

This is how it works with Java:

public static class Program {
    public static void main() {
        List<Foo> list = new ArrayList<>();
        for (Foo item : list) {
            if (item instanceof Bar) {
                new Combiner().combine((Foo & Bar) item);
            }
        }
    }
}

Created bug report for Kotlin: https://youtrack.jetbrains.com/issue/KT-25942

like image 520
Andrey Busik Avatar asked Aug 06 '18 18:08

Andrey Busik


2 Answers

If that's of any help, apparently if you write the same thing in Java:

abstract class Foo {
    public int getF() {
        return 1;
    }
}

interface Bar {
    default String getB() {
        return "a";
    }
}

static class Combiner {
    public <T extends Foo & Bar> Pair<Integer, String> combine(T arg) {
        return Pair.create(arg.getF(), arg.getB()); 
    }
}

public static class Program {
    public static void main() {
        List<Foo> list = new ArrayList<>();
        list.forEach(foo -> {
            if(foo instanceof Bar) {
                new Combiner().combine(foo);
            }
        });
    }
}

Then it won't work because of following message:

reason: no instance(s) of type variable(s) exist so that Foo conforms to Bar inference variable T has incompatible bounds: lower bounds: Foo upper bounds: Foo, Bar

Now if you add cast to Bar:

        list.forEach(foo -> {
            if(foo instanceof Bar) {
                new Combiner().combine((Bar)foo);
            }
        });

The problem is evident: (Bar)foo is now a Bar, and not a Foo.


So you'd need to know an exact type that is subclass of both Foo and Bar in order to cast to it.

So if this is the case, then following could work - in fact, in Java, it actually compiles:

public static <T extends Foo & Bar> T toBar(Foo foo) {
    //noinspection unchecked
    return (T)foo;
}

public static class Program {
    public static void main() {
        List<Foo> list = new ArrayList<>();
        list.forEach(foo -> {
            if(foo instanceof Bar) {
                new Combiner().combine(toBar(foo));
            }

In fact, the following test succeeds:

public static class Pair<S, T> {
    public Pair(S first, T second) {
        this.first = first;
        this.second = second;
    }

    S first;
    T second;

    public static <S, T> Pair<S, T> create(S first, T second) {
        return new Pair<>(first, second);
    }
}

public static <T extends Foo & Bar> T toBar(Foo foo) {
    //noinspection unchecked
    return (T)foo;
}

public class Blah extends Foo implements Bar {
}

@Test
public void castSucceeds() {
    Blah blah = new Blah();
    List<Foo> list = new ArrayList<>();
    list.add(blah);

    list.forEach(foo -> {
        if(foo instanceof Bar) {
            Pair<Integer, String> pair = new Combiner().combine(toBar(foo));
            assertThat(pair.first).isEqualTo(1);
            assertThat(pair.second).isEqualTo("a");
        }
    });
}

Which means the following should theoretically work in Kotlin:

class Program {
    fun main() {
        val list: List<Foo> = arrayListOf()
        list.forEach {
            if (it is Bar) {
                @Suppress("UNCHECKED_CAST")
                fun <T> Foo.castToBar(): T where T: Foo, T: Bar = this as T

                Combiner().combine(it.castToBar()) // <-- magic?
            }
        }
    }
}

Except it doesn't work, because it says:

Type inference failed: Not enough information to infer parameter T. Please specify it explicitly.

So in Kotlin, all I could do is this:

class Blah: Foo(), Bar {
}

Combiner().combine(it.castToBar<Blah>())

Which is clearly not guaranteed, only if we know the specific subtype of that is subclass of both Foo and Bar.


So I can't seem to find a way to make Kotlin cast a class to its own type and therefore "believe me" that it can be safely cast to a T that is itself and a subclass of both Foo and Bar.

But making Kotlin believe it through Java could work:

import kotlin.Pair;

public class ForceCombiner {
    private ForceCombiner() {
    }

    private static <T extends Foo & Bar> Pair<Integer, String> actuallyCombine(Bar bar) {
        T t = (T)bar;
        return new Combiner().combine(t);
    }

    public static Pair<Integer, String> combine(Bar bar) {
        return actuallyCombine(bar);
    }
}

And

class Program {
    fun main() {
        val list: List<Foo> = arrayListOf()
        list.forEach {
            if (it is Bar) {
                val pair = ForceCombiner.combine(it) // <-- should work

Except ForceCombiner now only works if we use @JvmDefault on the Kotlin interface

interface Bar {
    @JvmDefault
    val b: String get() = "a"
}

Which now says:

// Inheritance from an interface with `@JvmDefault` members is only allowed with -Xjvm-default option
class Blah: Foo(), Bar { 
}

So I haven't actually tried -Xjvm-default option, but it could work? See here how you can do that.

  • with -Xjvm-default=enable, only default method in interface is generated for each @JvmDefault method. In this mode, annotating an existing method with @JvmDefault can break binary compatibility, because it will effectively remove the method from the DefaultImpls class.
  • with -Xjvm-default=compatibility, in addition to the default interface method, a compatibility accessor is generated in the DefaultImpls class, that calls the default interface method via a synthetic accessor. In this mode, annotating an existing method with @JvmDefault is binary compatible, but results in more methods in bytecode.

Also, @JvmDefault requires target 1.8, but Android's desugaring should handle default interfaces now.

like image 127
EpicPandaForce Avatar answered Oct 24 '22 02:10

EpicPandaForce


The error message is somewhat obscure. Let's help Kotlin compiler with it a bit:

fun main(vararg args: String) {
     val list: List<Foo> = arrayListOf()
     list.forEach {
          val bar = it as Bar
          val c = Combiner().combine(bar) // inferred type Bar is not a subtype of Foo
     }
}

What smart casts are doing is just that - they're casting. Once you cast Foo into Bar, there's no guarantee that it's a Foo anymore.

like image 25
Alexey Soshin Avatar answered Oct 24 '22 03:10

Alexey Soshin