Support for "satisfies an interface"

I want to write a function, that accepts any argument that just fullfills or satisfies an interface.

E.g. My example interface Inf1 has an open() and a close() method.

fun myFun(arg: satisfies Inf1) {arg.open(); … arg.close();}
myFun(anyObj);

How is this different than fun myFun(arg: Inf1)? Do you mean duck typing?

Yes, exactly, different wording.

Since Kotlin is a statically typed language it should be easy for the compiler to check for fulfillment?

If you have an array of Any and use the elements with that function then of course I would expect an error at compile time: “Type Any does not satisfy interface Inf1”

Also see: How To Use Structural Types in Scala | Baeldung on Scala

idk if I want duck typing in Kotlin. I like being able to explicitly see what type is being used.

Without duck typing, you need to use reflection or create adapter wrappers for foreign classes to provide common functionality.

E.g. you can run a function render() on an object without knowledge of an explicit interface.
The SAM pattern is already working “duck”-ish. :wink: You do not need to explicitly implement an interface.

1 Like

Just write an adapter. Given:

interface Inf1 {
    fun open()
    fun close()
}

fun myFun(arg: Inf1) {
    arg.open()
    arg.close()
}

// does not implement Inf1
class UnrelatedClass { 
    fun open() = println("open")
    fun close() = println("close")
}

You can have

class Inf1Adapter(val open: () -> Unit, val close: () -> Unit) : Inf1 {
    override fun open() {
        open.invoke()
    }

    override fun close() {
        close.invoke()
    }
}

...
val unrelated = UnrelatedClass()
myFun(Inf1Adapter(unrelated::open, unrelated::close))

2 Likes

Yes, nice example.
But this boilerplate code could be avoided.
You “emulate” duck typing by using 2 SAM implementation, one for each interface method signature.

You can even take this a step further with contexts:

interface Inf1<T> {
    fun T.open()
    fun T.close()
}

context(i: Inf1<T>)
fun <T> T.open() = with(i) { open() }
context(i: Inf1<T>)
fun <T> T.close() = with(i) { close() }

context(_: Inf1<T>)
fun <T> myFun(arg: T) {
    arg.open()
    arg.close()
}

// does not implement Inf1
class UnrelatedClass { 
    fun open() = println("open")
    fun close() = println("close")
}

private fun UnrelatedClass.openAliased() = open()
private fun UnrelatedClass.closeAliased() = close()
object UnrelatedClassInf1Adapter : Inf1<UnrelatedClass> {
    override fun UnrelatedClass.open() {
        openAliased()
    }

    override fun T.close() {
        closeAliased()
    }
}

...
val unrelated = UnrelatedClass()
context(UnrelatedClassInf1Adapter) {
    myFun(unrelated)
}

That’s why I prefer just a single keyword: “satisfies” :grinning_face_with_smiling_eyes:

fun myFun(arg: satisfies Inf1)

What makes me very uncomfortable about duck-typing like this is that an interface is more than just a method (or property) signature; it’s also a contract, telling what the method does, what it means, why and when you’d call it, what preconditions it might need and what it guarantees or tries to give you in return.

A name isn’t always enough to tell you all of that. A verb might mean very different things in different cases, and trying to lump them all together like that is asking for trouble IMO.

1 Like

For this reason maybe it would be better to do the trick not on the def-site, but on the call-site, e.g.: myFun(anyObj wrapAs Inf1). A little more cumbersome, but more explicit and flexible as we can use this in many different scenarios. And because the caller knows both types, they can make a conscious decision on whether the object actually satisfies the interface. But I don’t know how other languages solved that problem.

1 Like

Here is where I think there is an opportunity for Kottin to do something. Kotlin has class delegation which lets you implement an interface by delegating to another instance. But it requires that the other instance implement that interface. It could have a way to do delegation to an object that does not implement the interface, but just has methods that match. There would need to be special syntax to do this.

This would not be full on duck typing. It would be a sort of compile time form that just generates code like this instead of writing it out

2 Likes

Feels like there’s room for a KSP plugin here, or a compiler plugin

2 Likes

Hmm, yes, but on the def side you can decide better if you can handle objects that only satisfy the interface.

Maybe in this case not Kotlin but Java has the solution. An ugly, dangerous solution, but nonetheless:

class DuckInvocationHandler(val obj: Any) : InvocationHandler {

    override fun invoke(proxy: Any?, m: Method, args: Array<Any?>?): Any? =
        try {
            // yeah, this is crude, just a proof of concept  
            val objM = obj.javaClass.methods.first { 
                it.name == m.name && it.parameters.size == it.parameters.size 
            }
            objM.invoke(obj, *(args ?: arrayOf()))
        } catch (e: InvocationTargetException) {
            throw e.getTargetException()
        } catch (e: Exception) {
            throw RuntimeException("unexpected invocation exception: ${e.message}")
        }
}

inline fun <reified Duck> Any.duckType(): Duck = Proxy.newProxyInstance(
    javaClass.getClassLoader(),
    arrayOf(Duck::class.java),
    DuckInvocationHandler(this)
) as Duck

With this monstrosity at hand, we can write:

val unrelated = UnrelatedClass()
myFun(unrelated.duckType())

And voilà, duck typing!

1 Like

Yes but this is reflection based and slow.

(In particular: if any methods aren’t implemented, or have an incompatible return type, that gives no compile errors, just confusing exceptions at runtime. :tired_face: Kotlin is better than that!)

Of course this could be made a little safer, e.g. we could already check in duckType() that the given object contains all methods of the interface. It’s still only a runtime error, but it could point out precisely what went wrong, and fail early.

The only way to get static checking would be KSP or a compiler plugin (as kyay10 already suggested).