DEV Community

Ranil Wijeyratne
Ranil Wijeyratne

Posted on

Functional interfaces: Self-loathing in Kotlin

This post is a copy from previous posts on Medium (initial, follow-up) But since I'm planning on deleting my Medium account I moved them here.

Kotlin is a wonderful programming language. After roughly 12 years of Java programming working with Kotlin felt like putting on glasses after years of squinting: there's so much to love.

But like with every relationship, some of the quirks you only discover later in your life together. After migrating more and more Java code to Kotlin code, I noticed something rather odd and frankly a bit annoying.

It's the way Kotlin handles functional interfaces.

Java 7: a blast from the past

Let's go back in time to a world without lambdas. It was terribly verbose!

interface JavaInterface {
    String doSomething(Item item);
}

String delegateWork(JavaInterface f) {
    return f.doSomething(item);
}

void doWork() {
    delegateWork(new JavaInterface() {
        @Override
        public String doSomething(Item item) {
            return "Item = " + item;
        }
    });
}

Java 8: Lambdas to the rescue!

Finally Java 8 gave us Lambdas and we could get rid of a lot of code and focus on what's important. Also we weren't forced to write our own functional interface for every simple function we could just use some that oracle provided such as: java.util.function.Function<T, R>

@FunctionalInterface
interface JavaInterface {
    String doSomething(Item item);
}

String delegateWork(JavaInterface f) {
    return f.doSomething(item);
}

String delegateOtherWork(Function<Item, String> f) {
    return f.apply(item);
}

void doWork() {
    delegateWork(item -> "Item = " + item);
    delegateOtherWork(item -> "Item = " + item);
}

Things were nice until you realised that even though you now had function types they still weren't first-class citizen in the language. Want proof? Guess how many "Types of Functions" had to be introduced in Java? One? Three? Five?

43!

Don't believe me, see for yourself: https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html

And if that's not enough for you, add jOOL to the mix and you have access to 35 more: https://github.com/jOOQ/jOOL/tree/master/jOOL/src/main/java/org/jooq/lambda/function
Because who wouldn't love coming across a method signature that looks like this:

Function5<String, String, String, String, String, Tuple3<String, String, String>> higherOrder(Function12<String, Integer, String, Object, Object, Object, BiFunction<String, Integer, String>, String, Integer, Long, String, Double, Optional<Tuple2<String, String>>>)

😜 side note: jOOL is actually quite a neat library, and worth checking out.

Kotlin help us!

Now let's add Kotlin to the mix. In Kotlin functions are first-class citizen. So no need to remember dozens of slightly different function types. You just need to remember Kotlin's Function Type Syntax:

(Parameter1Type, Parameter2Type, ParameterNType) -> ReturnType

That's it, that's all there's to it.

Trouble in paradise

So ok, why are we here, what's the problem?

As mentioned earlier, as I migrated more and more code from Java to Kotlin. I came across some issues when working with custom functional interfaces. Because sometimes you want that additional descriptiveness.

Let's go back to our Java 8 example

@FunctionalInterface
interface JavaInterface {

    String doSomething(Item item);
}

class JavaComponent {

    private Item item = new Item();

    String delegateWork(JavaInterface f) {
        return f.doSomething(item);
    }

    String delegateOtherWork(Function<Item, String> f) {
        return f.apply(item);
    }
}

Now let's use it from Kotlin code

delegateWork { "Print $it" }
delegateOtherWork { "Print $it" }

Nice this is great, just what we expected! Ok now let's migrate this JavaComponent class to Kotlin. Notice we've changed the java.util.function.Function<Item, String> to a Kotlin function type (Item) -> String

class KotlinComponent(private val item: Item = Item()) {

    fun delegateWork(f: JavaInterface): String {
        return f.doSomething(item)
    }

    fun delegateOtherWork(f: (Item) -> String): String {
        return f.invoke(item)
    }
}

Let's see what happens when we use these higher order functions from Java code.

delegateWork(item -> "Print: " + item);
delegateOtherWork(item -> "Print: " + item);

Nothing out of the ordinary as expected we can use the same lambda for both methods. Let's see what happens when we do what we'd expect in Kotlin:

delegateWork { "Print $it" }

Error: Kotlin: Type mismatch: inferred type is () -> String but JavaInterface was expected

What happened? It seems the compiler can't figure out that the signature of the lambda is the same as the functional interface method. https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions

So we have to explicitly say what we expect:

delegateWork(JavaInterface { "Print $it" })

I think this is rather disappointing but it's not too bad. Now let's see what happens when we also migrate the interface to Kotlin:

interface KotlinInterface {
    fun doSomething(item: Item): String
}

class KotlinComponent(private val item: Item = Item()) {

    fun delegateWork(f: KotlinInterface): String {
        return f.doSomething(item)
    }

    fun delegateOtherWork(f: (Item) -> String): String {
        return f.invoke(item)
    }
}

When we use the KotlinComponent class from Java, as expected nothing changes, the lambdas remain exactly the same. What if we use it from Kotlin code:

delegateWork { "Print $it" }

Error: Kotlin: Type mismatch: inferred type is () -> String but KotlinInterface was expected

It seems the SAM conversion fails again. Now what if we just explicitly mention the Interface like we did before?

delegateWork(KotlinInterface { "Print $it" })

Error: Kotlin: Interface KotlinInterface does not have constructors

This didn't help either. We need to create an anonymous object to make it work:

delegateWork(object : KotlinInterface {
    override fun doSomething(item: Item): String {
        return "Print $item"
    }
})

Yikes! This feels like working with Java 7 all over again. Sadly this is because Kotlin doesn't yet support SAM conversion for Kotlin interfaces so we have to create this anonymous object. See also:
https://youtrack.jetbrains.com/issue/KT-7770
https://stackoverflow.com/a/43737962/611032

Alias time!

So how can we avoid these verbose anonymous objects and still use a custom name for the function? We use a type alias:

/**
 * Very helpful comment.
 */
typealias KotlinFunctionAlias = (Item) -> String

fun delegateAliasWork(f: KotlinFunctionAlias): String {
  return f.invoke(item)
}

So now we can pass in a lambda the way we'd expect and we still have the benefit of a custom name for the function.

delegateAliasWork { "Print $it" }

So all is is well then, case closed, time to go home. Unfortunately not quite.

Lost in translation

One minor issue with type aliases is that while you can name the function type, you cannot name the method name:

val iface: JavaInterface = JavaInterface { "Print $it" }
iface.doSomething(item)

val alias: KotlinFunctionalAlias = { item -> "Print $item" }
alias.invoke(item)
alias(item)

Choosing good names for the type alias and variable can mitigate the issue. Luckily we developers are great at naming things 😜

Type safety

The bigger issue is that while the type alias gives us a different name, they aren't really different types, so we're not actually type safe.

Let's look at a Java example with two functional interfaces that have the same method signature.

JavaInterface1 f1 = item -> "Print " + item;
JavaInterface2 f2 = item -> "Print " + item;
f1 = f2;

Error: java: incompatible types: JavaInterface2 cannot be converted to JavaInterface1

This is what we'd expect we don't want to mix apples and oranges here.

What happens if we do the same thing with our Kotlin type aliases? (I think you know where I'm going with this)

var f1: KotlinFunctionAlias1 = { item -> "Print $item" }
var f2: KotlinFunctionAlias2 = { item -> "Print $item" }
var f3: (Item) -> String = { item -> "Print $item" }
f1 = f2
f2 = f3
f1 = f3

This works fine, the compiler doesn't complain because like I mentioned they aren't actually different types. They're all simply: (Item) -> String

Solutions

So let's quickly recap the different ways we can deal with Kotlin's missing SAM conversion for Kotlin interfaces and their upsides and downsides

Leave functional interfaces as Java interfaces

+ Good Java interoperability
+ Support for custom method name
+ Type safe

- Need to prefix Kotlin lambda with interface name
- Additional parentheses needed
- Need to maintain Java code

Use a type alias for Kotlin function types

+ Good Java interoperability
+ Easy to use

- Not type safe
- No custom method name

Use inline classes

Another option we haven't yet discussed is the use of the experimental Kotlin inline classes. You could "wrap" a Kotlin Function with an inline class.

inline class KotlinInlineInterface(val doSomething: (Item) -> String)

fun delegateInlineWork(f: KotlinInlineInterface): String {
    return f.doSomething.invoke(item)
}

delegateInlineWork(KotlinInlineInterface { "Print $it" })

Even though this works, I don't thinks it's an appropriate way of using inline classes. Also Java interoperability isn't currently supported: https://kotlinlang.org/docs/reference/inline-classes.html#mangling

Always use Kotlin function types

Yes you could just use (ParamT) -> ReturnT types everywhere. Often that will be sufficient but as your application grows it might get harder to read and maintain and more error-prone.

Live with anonymous objects

Of course if you don't mind, you can just live with the anonymous objects, hope that someday Kotlin will support full SAM conversion and make use of the wonderful IDE integration to migrate your anonymous objects to lambdas

¯\(ツ)/¯

Jetbrains Feedback

There has been a short discussion on Reddit: https://www.reddit.com/r/Kotlin/comments/bipj0q/functional_interfaces_selfloathing_in_kotlin/

Since then I got a response from Roman Elizarov on the subject

I tried the mentioned Kotlin compiler option:

// Gradle Kotlin DSL
tasks.withType<KotlinCompile> {
    kotlinOptions.freeCompilerArgs += "-XXLanguage:+NewInference"
}
// Gradle Groovy DSL
compileKotlin {
    kotlinOptions {
        freeCompilerArgs += "-XXLanguage:+NewInference"
    }
}

If you're more into other build systems, refer to Kotlin documentation (Maven / Ant) to see how to pass Kotlin compiler arguments.

Problem solved?

First let's see what happens when we use a Kotlin functional interface in Kotlin code:

fun delegateWork(f: KotlinInterface): String {
    return f.doSomething(item)
}

delegateWork { item -> "Print: $item" }

Error: Type mismatch: inferred type is (Nothing) -> TypeVariable(_L) but KotlinInterface was expected

What about explicitly specifying the interface?

delegateWork(KotlinInterface { item -> "Print $item" }

Error: Interface KotlinInterface does not have constructors

Bummer! We still need an anonymous object.

What about using a Java functional interface in Kotlin code?

fun javaInterface(f: JavaInterface) {
    val res = f.doSomething(item)
    output(res)
}

javaInterface { item -> "Print: $item" }

Finally: just what we expected. All is well, beer well deserved!

Patience young Jedi

If you're observant, you'll see this during the build:

w: ATTENTION!
This build uses unsafe internal compiler arguments:
-XXLanguage:+NewInference

This mode is not recommended for production use,
as no stability/compatibility guarantees are given on
compiler or generated code. Use it at your own risk!

So what does that mean? It means what it says here: this isn't really safe to use yet. But knowing JetBrains is working in that direction I'd suggest that we, for now, do things the following way (most favourable to least favourable)

  1. Keep functional interfaces as Java code
  2. Use type aliases for Kotlin function types (if you can live with potentially mixing apples and oranges)
  3. Live with the anonymous objects

Thanks for reading. As always I'm open to criticism and feedback.

Top comments (0)