Search code examples
kotlingenericstype-inferencecovariance

Kotlin type inference on "supposedly" right types


I am new to Kotlin and I was playing with it. I pretty much wanted to create a pretty basic event bus. So I came up with this

interface Event
interface EventListener<E : Event> {
    fun handle(event: E)
}

interface EventBus {
    fun <E : Event> registerListener(aClass: Class<E>, eventListener: EventListener<E>)
}

class MyBus() : EventBus {
    private val eventListeners: MutableMap<String, MutableList<EventListener<out Event>>> = mutableMapOf()

    constructor(listeners: List<Pair<Class<Event>, EventListener<Event>>>) : this() {
        listeners.forEach {
            registerListener(it.first, it.second)
        }
    }

    override fun <E : Event> registerListener(aClass: Class<E>, eventListener: EventListener<E>) {
        val key = aClass.name
        val listeners: MutableList<EventListener<out Event>> = eventListeners.getOrPut(key) { mutableListOf() }
        listeners.add(eventListener)
    }
}


val bus = MyBus(
    listOf(
        MyEvent::class.java to MyEventListener()
    )
)

class MyEvent : Event
class AnotherEvent : Event
class MyEventListener : EventListener<MyEvent> {
    override fun handle(event: MyEvent) {
    }
}

what happens is that when I try to create MyBus using the constructor accepting the list of pairs, I get

Type inference failed. Expected type mismatch: inferred type is List<Pair<Class<MyEvent>,MyEventListener>> but List<Pair<Class<Event>,EventListener<Event>>> was expected

But if I change the constructor to be something like

constructor(listeners: List<Pair<Class<out Event>, EventListener<out Event>>>) : this() {
        listeners.forEach {
            registerListener(it.first, it.second)
        }
    }

adding out pretty much everywhere, then the MyBus constructor works, but the invocation to registerListener(..) breaks for the same exact reason as before. So the only way to solve this is to add "out"s also on registerListener function.

I suspect I'm doing something wrong here, but I don't know what precisely. Any help?


Solution

  • If you want your EventListener to be able to consume Events, then its type has to be invariant or covariant (not declared out). If it let you pass your EventListener<MyEvent> as if it were an EventListener<Event>, then your MyBus class might call listener.handle(event) on it with some Event that is not a MyEvent, such as AnotherEvent. Then you will get a ClassCastException when it tries to cast this AnotherEvent to MyEvent.

    To be able to store different types of invariant EventHandlers, you will have to remove the variance restrictions by using star projection, and cast them when you retrieve them from the map. So make the map keys into class objects instead of just Strings. Since you will not have the help of the compiler when working with the star-projected types, you need to be careful that you are only adding an item to your MutableMap that is of the same type as the Class key that's associated with it. Then when you retrieve items, only cast to an invariant type.

    The other part of your issue is that your constructor needs a generic type. Right now it works exclusively with Event so it can't handle subtypes of Event. Kotlin doesn't (yet?) support generic types for constructors so you have to do this with a factory function.

    Here's an example of all the above.

    class MyBus() : EventBus {
        private val eventListeners: MutableMap<Class<*>, MutableList<EventListener<*>>> = mutableMapOf()
    
        override fun <E : Event> registerListener(aClass: Class<E>, eventListener: EventListener<E>) {
            val listeners = retrieveListeners(aClass)
            listeners.add(eventListener)
        }
    
        private fun <E: Event> retrieveListeners(aClass: Class<E>): MutableList<EventListener<E>> {
            @Suppress("UNCHECKED_CAST")
            return eventListeners.getOrPut(aClass) { mutableListOf() } as MutableList<EventListener<E>>
        }
    }
    
    // Factory function
    fun <E : Event> myBusOf(listeners: List<Pair<Class<E>, EventListener<E>>>): MyBus {
        return MyBus().apply {
            listeners.forEach {
                registerListener(it.first, it.second)
            }
        }
    }
    

    And you might want to change the type of the factory parameter from a <List>Pair to a vararg Pair so it's easier to use.


    Here's a stripped down example to explain the variance limitation.

    Your interface for an Event consumer:

    interface EventListener<E : Event> {
        fun handle(event: E)
    }
    

    Two implementations of Event:

    class HelloEvent: Event {
       fun sayHello() = println("Hello world")
    }
    
    class BoringEvent: Event {}
    

    A class implementing the interface:

    class HelloEventListener: EventListener<HelloEvent> {
        override fun handle(event: HelloEvent) {
            event.sayHello()
        }
    }
    

    Now you have an EventListener that can handle only HelloEvents. Try to treat it like an EventListener<Event>:

    val eventListener: EventListener<Event> = HelloEventListener() // COMPILE ERROR!
    

    Imagine the compiler did not prevent you from doing this and you do this:

    val eventListener: EventListener<Event> = HelloEventListener()
    eventListener.handle(BoringEvent()) // CLASS CAST EXCEPTION AT RUN TIME!
    

    If this were allowed your HelloEventListener would try to call sayHello() on the BoringEvent, which doesn't have that function, so it will crash. This is what generics are here to protect you from.

    Now suppose your HelloEventListener.handle() didn't call event.sayHello(). Well, then it could have safely handled a BoringEvent. But the compiler isn't doing that level of analysis for you. It just knows what you declared, that HelloEventListener cannot handle anything except HelloEvent.