Search code examples
kotlingenericsnested-generics

Kotlin generics - how to set up non-trivial relations? (expected Nothing)


I would like to create a simple API for processing in-app events (sending their info to various targets).

I would like to have that type-based, so that the event processor would know where to send which event would be determined by the sub-type of the event.

Here is an example:

interface AppEvent

interface Sender<T: AppEvent> {
    fun send(event: T)
    val acceptsAbility: KClass<T>
}


class Loggable: AppEvent, HasMessage

interface HasMessage { val message: String }


class LogSender : Sender<Loggable> {
    override fun send(event: Loggable) = Logger(...).info(event.message)
    override val acceptsAbility = Loggable::class
}

fun main() {
    val dispatcher = Dispatcher<AppEvent>()
    val sender: Sender<Loggable> = LogSender()
    dispatcher.addSender(sender)
}

A particular event should then be a mixture of various ...able interfaces, which would determine, which all Senders would send them. This would be used by the dispatcher class: If the given event matches the interface of some sender, then pass it to that sender. In Kotlin, this needs to be checked at runtime.

class ImportErrorEvent : AppEvent, Loggable

Note to that - I would prefer Loggable not to inherit from AppEvent, but that would need the TypeScript way of type compatibility, where just having the right mix of properties and functions is enough to match the type requirement.

So I attempted to code a basic dispatcher, but ran into type generics issues.

class Dispatcher <T: AppEvent> {
    val senders: MutableList<Sender<out T>> = mutableListOf()

    fun <TF: Sender<out T>> addSender(a: TF) { senders.add(a) }

    fun dispatch(event: T) {
        if (event is sender.acceptedAbility)
                senders
            .filter { it.acceptsAbility.isInstance(event) }
            .forEach { it.send(event as AppEvent)} // This type check needs to be relaxed.
    }
}

class Dispatcher2 {
    val senders: MutableList<Sender<out AppEvent>> = mutableListOf()

    fun <T: AppEvent, TS: Sender<out T>> addSender(a: TS) { senders.add(a) }

    fun dispatch(event: Any) {
        senders
            .filter { it.acceptsAbility.isInstance(event) }
            .forEach { it.send(event as AppEvent)} // This type check needs to be relaxed.
    }
}

Both of these have a compilation error in senders.forEach { it.send(event) }:

Type mismatch.
   Required: Nothing
   Found: Any

I have the same idea implemented in Java using extends and super.

What would be the right way to code this idea with generic?
Is there some extended documentation on Kotlin generics I could dive into?

Edit:

The type safety is intended to be partially done at runtime:

TheDispatcher would

  1. Ask the sender for the type it can consume (e.g. Loggable),
  2. Check if the event implements that, or if it has all the required traits,
  3. Pass it to the sender if matched, with either type-casting, or re-wrapping.

I have changed the code above to include the type check.

So I am looking for a way to relax the type checking while still imposing some constraints for the calling code.


Solution

  • If I understand you, you have to write something like this:

    interface AppEvent
    
    interface Sender<in T : AppEvent> {
        fun send(event: T)
    }
    
    interface HasMessage {
        val message: String
    }
    
    class Loggable : AppEvent, HasMessage {
        override val message: String = ""
    }
    
    class Testable : AppEvent
    
    class LogSender : Sender<Loggable> {
        override fun send(event: Loggable) = Unit
    }
    
    fun main() {
        val station = Dispatcher()
        val feeder: Sender<Loggable> = LogSender()
        station.addSender(feeder)
        station.dispatch(Loggable()) // It will be send
        station.dispatch(Testable()) // It will not send
    }
    
    class Dispatcher {
        private val senders = mutableListOf<Sender<AppEvent>>()
    
        inline fun <reified T : AppEvent> addSender(sender: Sender<T>) {
            val wrapper: Sender<AppEvent> = object : Sender<AppEvent> {
                override fun send(event: AppEvent) {
                    if (T::class.isInstance(event)) {
                        sender.send(event as T)
                    }
                }
            }
            addCommonSender(wrapper)
        }
    
        fun addCommonSender(sender: Sender<AppEvent>) {
            senders.add(sender)
        }
    
        fun dispatch(event: AppEvent) {
            for (sender in senders) {
                sender.send(event)
            }
        }
    }
    

    inline + reified allows you to capture sender's type of T. After it you can create wrapper which will implements Sender and cast it to T after runtime check.

    Finally you have type independent dispatcher and strongly typed senders.

    The other option it's strongly typed dispatcher:

    class Dispatcher2 <T: AppEvent> {
        val senders: MutableList<Sender<T>> = mutableListOf()
    
        fun addSender(a: Sender<T>) { senders.add(a) }
    
        fun dispatch(event: T) {
            for(sender in senders) {
                sender.send(event)
            }
        }
    }
    

    It could be used like this:

    fun main() {
        val station = Dispatcher2<Loggable>()
        val feeder: Sender<Loggable> = LogSender()
        station.addSender(feeder)
        station.dispatch(Loggable()) // Dispatched
        station.dispatch(ImportErrorEvent()) // Dispatched too
    }