Home > Mobile >  Make class extend multiple function type interfaces with the same signature in Kotlin
Make class extend multiple function type interfaces with the same signature in Kotlin

Time:07-28

I want to make a class extend multiple function type interfaces.

This works since the function types have different signatures, () -> Unit and (String) - Unit

typealias A = () -> Unit
typealias B = (something: String) -> Unit

class Test : A, B {

    override fun invoke() {
        TODO("Not yet implemented")
    }

    override fun invoke(something: String) {
        TODO("Not yet implemented")
    }

}

Now if I add a third function type, the compiler complains about Conflicting Overloads or A supertype appears twice

typealias A = () -> Unit
typealias B = (something: String) -> Unit
typealias C = (somethingElse: String) -> Unit

class Test : A, B, C {

    override fun invoke() {
        TODO("Not yet implemented")
    }

    override fun invoke(something: String) {
        TODO("Not yet implemented")
    }

    override fun invoke(somethingElse: String) {
        TODO("Not yet implemented")
    }
    
}

I can obviously go and add garbage params to C to make it work, but this seems more like a hack

typealias C = (somethingElse: String, garbage: Unit?) -> Unit

but now if I define type D with the same signature,

typealias D = (somethingElseElse: String, garbage: Unit?) -> Unit

I would run into the same issue.

I thought that maybe value classes could help here:

@JvmInline
value class BString(val value: String)

@JvmInline
value class CString(val value: String)

typealias A = () -> Unit
typealias B = (something: BString) -> Unit
typealias C = (somethingElse: CString) -> Unit

class Test : A, B, C {
    override fun invoke() {
        TODO("Not yet implemented")
    }

    override fun invoke(something: BString) {
        TODO("Not yet implemented")
    }

    override fun invoke(somethingElse: CString) {
        TODO("Not yet implemented")
    }

}

... but since value classes are compiled out of existence, that too is not a solution

Platform declaration clash: The following declarations have the same JVM signature (invoke(Ljava/lang/Object;)Ljava/lang/Object;):

I'm assuming Kotlin KEEP 302, Binary Signature Name (https://github.com/Kotlin/KEEP/blob/binary-signature/proposals/multiplatform/binary-signature.md), would solve this issue in the future, but what is the correct way in the meantime to implement multiple function interfaces with the same signatures?

Practical use-case that I can think of: let's say you want to have a class that can handle Clickable and DoubleClickable, both would have something like (Event) -> Unit

EDIT: based on @mateusz's answer, this works, but only when using value classes, not if interface B and C are using normal Strings:

@JvmInline
value class BString(val value: String)

@JvmInline
value class CString(val value: String)

interface A {
    operator fun invoke()
}

interface B {
    operator fun invoke(something: BString)
}

interface C {
    operator fun invoke(somethingElse: CString)
}

class Test : A, B, C {

    override operator fun invoke() {
        println("invoke A")
    }

    override operator fun invoke(something: BString) {
        println("invoke B - something = $something")
    }

    override operator fun invoke(somethingElse: CString) {
        println("invoke C - somethingElse = $somethingElse")
    }

}

fun main(args: Array<String>) {

    val handlerA = A::invoke
    val handlerB = B::invoke
    val handlerC = C::invoke

    val t = Test()

    handlerA(t) 
    handlerB(t, BString("hello B"))
    handlerC(t, CString("hello C"))

}

outputs:

invoke A

invoke B - something = BString(value=hello B)

invoke C -somethingElse = CString(value=hello C)

CodePudding user response:

The completer does not care about parameter's names. The fun test(a: String): String and fun test(b: String): String are the same functions. When you will call test("some") then which function should be called?

You can create dedicated interfaces:

interface Clickable {
    fun click(param: String)
}

interface DoubleClickable {
    fun fastDoubleClick(param: String)
    fun slowDoubleClick(param: String)
}

Then you can use function references if you want val handleClickFun: String -> Unit = Clickable::click

CodePudding user response:

This will never work. At the fundamental JVM level, you can't implement the same interface twice with different generics. I would not expect this to ever work, even with the KEEP you mention.

Why do you want to extend function interfaces at all? If you just want the nice call syntax, you can have separate operator fun invoke overloads, without overriding anything. But even better would be using functions with actual names. If you need to pass it to methods accepting lambdas, use method references, e.g. Test::handleClick and Test::handleDoubleClick.

CodePudding user response:

A typealias is just a way to give a convenient label to a specific type - it's not a type in itself, anywhere you specify that typealias, you can can just pass in a variable defined as the real type, or any other typealias you've derived from it.

So B and C are the same thing. You can have two different aliases for the same thing if that makes sense in different parts of your code (that's kinda the whole point of them! Relabel types to make them more readable or understandable) but that's just ways to refer to a type.

But when it comes to defining your class, it makes no sense. B and C are the same type, you're repeating yourself (and the compiler will give you a supertype appears twice error). And to implement that one type, you need one function - and only one, because if you have two identical functions then which one would get called?

So you can do this if you want:

typealias A = () -> Unit
typealias B = (something: String) -> Unit
typealias C = (somethingElse: String) -> Unit

class Test : A, B {

    override fun invoke() {
        println("invoke")
    }

    override fun invoke(something: String) {
        println("invoke: $something")
    }    
}

fun doAThing(thing: C) {
    thing("wow")
}


fun main() {
    doAThing(Test())
}

doAThing takes a C, so we can pass it a B, because B is C.


I'm guessing that's not very useful to you, but that's the limitation of typealiases, and bare function types in general. If you want two separate functions with the exact same signature in the same scope, you need to be able to refer to them explicitly - and that usually means giving them different names.

How is your click-handler class going to handle your Event if you can't tell it whether it's a single or double-click? And even if you could (e.g. through something like (handlerFunction as B).invoke(event)) then which of your identical overridden functions in the class is which?

Like Mateusz says, you need to use interfaces, and then you can pass references to the functions, because you have a name for each one you can refer to. The things you're passing those functions into can define the types using typealiases if they want. And if you want a type that can handle both kinds of clicks, create another interface that implements both types.

If you want to be able to pass a single object that has multiple functions with the same signature, that's what you need. If you want to use function types instead, you'll have to pass the individual function references in - but something somewhere has to be able to distinguish between them in the first place, and that's usually where they're defined

  • Related