Can't replace SAM-constructor with lambda when first argument is a class with one method

arekolek :

I’m puzzled over SAM constructors, I have this Java class:

public class TestSam<T> {

    public void observe(ZeroMethods zero, Observer<T> observer) {
    }

    public void observe(OneMethod one, Observer<T> observer) {
    }

    public void observe(TwoMethods two, Observer<T> observer) {
    }

    public interface Observer<T> {
        void onChanged(@Nullable T t);
    }

    public interface ZeroMethods {
    }

    public interface OneMethod {
        First getFirst();
    }

    public interface TwoMethods {
        First getFirst();

        Second getSecond();
    }

    public interface First {
    }

    public interface Second {
    }
}

And this Kotlin code:

fun testSam(
        test: TestSam<String>,
        zero: TestSam.ZeroMethods,
        one: TestSam.OneMethod,
        two: TestSam.TwoMethods
) {
    test.observe(zero) { println("onChanged $it") } // 1. compiles
    test.observe(zero, TestSam.Observer { println("onChanged $it") }) // 2. Redundant SAM-constructor

    test.observe(one) { println("onChanged $it") } // 3. doesn't compile
    test.observe({ one.first }) { println("onChanged $it") } // 4. compiles
    test.observe(one, TestSam.Observer { println("onChanged $it") }) // 5. compiles

    test.observe(two) { println("onChanged $it") } // 6. compiles
    test.observe(two, TestSam.Observer { println("onChanged $it") }) // 7. Redundant SAM-constructor
}

What's the deal here? Why can't Kotlin figure out 3. (and provides the special variant 4.), but handles all other cases?


The rationale for this code is LiveData<T>.observe(LifecycleOwner owner, Observer<T> observer) method in Android, where LifecycleOwner has one method getLifecycle().

arekolek :

This will be fixed in Kotlin 1.4, see https://blog.jetbrains.com/kotlin/2020/03/kotlin-1-4-m1-released/

Kotlin has supported SAM conversions for Java interfaces from the beginning, but there was one case that wasn’t supported, which was sometimes annoying when working with existing Java libraries. If you called a Java method that took two SAM interfaces as parameters, both arguments need to be either lambdas or regular objects. It wasn’t possible to pass one argument as a lambda and another as an object. The new algorithm fixes this issue, and you can pass a lambda instead of a SAM interface in any case, which is the way you’d naturally expect it to work.


With the upcoming new type inference, this issue will be fixed in the Kotlin compiler. Experimental type inference can be enabled now (Kotlin 1.3 required) in an Android project by adding this to your module-level gradle file:

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
    kotlinOptions {
        freeCompilerArgs = ["-XXLanguage:+NewInference"]
    }
}

It is supposed to be possible to enable it with just (but it doesn't quite work yet):

kotlin {
    experimental {
        newInference = "enable"
    }
}

Beholder wrote:

The cause of this is technical difficulty in compiler design

The way I see it, Kotlin compiler doesn't want to generate 2^n variants for a method that has n parameters that are eligible for SAM conversion, so instead it generates only two variants: the one where all are lambdas and the one where there are no lambdas

There's a related issue on YouTrack: Impossible to pass not all SAM argument as function

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=441353&siteId=1
Recommended