When Type Inference Doesn't Cut It cover image

When Type Inference Doesn't Cut It

David Kanenwisher • March 8, 2020

kotlin

In order to learn Kotlin I've been working my way through a goofy program called "adventure-venture". I honestly don't have a clear idea of what it does but so far it has adventurers in a tavern that want to talk to each other. I'm also trying to understand event based systems so the adventurers interact with the world through events.

In the process of trying to get the adventurers to talk to each other I've been refactoring the way the system works. When I refactor I like to try and keep all of the tests passing as I advance forward with the refactoring. That way it's clear when I am changing behavior and when I am not. It's in this way that I discovered a situation where I can't rely on type inference.

I got my code to the point where I had these two tests:

    @Test
    fun `it can dispatch an empty event object`() {
        val dispatcher = Dispatcher()

        var passedValue = EmptyEvent()

        dispatcher.eventSubscribe { event -> passedValue = event }

        assertEquals("", passedValue.name)
    }

    @Test
    fun `it can dispatch an emitted event object`() {
        val dispatcher = Dispatcher()

        var passedValue = EmittedEvent("emittedSound")

        dispatcher.eventSubscribe { event -> passedValue = event }

        assertEquals("emittedSound", passedValue.name)
    }

Notice that in the first test I have var passedValue = EmptyEvent() and in the second test I have a similar line var passedValue = EmittedEvent("emittedSound"). Why not just leave them like this then? Well, they don't compile because dispatcher.eventSubscribe expects a lambda of type (Event) -> Unit. I thought I had this covered by creating the Event interface and making each implement this interface. No dice, I starting getting Type mismatch errors from Intellij on this line:

        dispatcher.eventSubscribe { event -> passedValue = event }

After staring at my code a bit a thought occurred to me. The type of event above is Event while the type of passedValue is either EmptyEvent or EmittedEvent. This means I must not be able to assign a more specific type, EmptyEvent or EmittedEvent, to a less specific type Event. This lead to me think I need to declare the type as Event so that passedValue can always hold an Event.

I added explicit type information to passedValue by changing it to passedValue: Event like so:

    @Test
    fun `it can dispatch an empty event object`() {
        val dispatcher = Dispatcher()

        var passedValue: Event = EmptyEvent()

        dispatcher.eventSubscribe { event -> passedValue = event }

        assertEquals("", passedValue.name)
    }

    @Test
    fun `it can dispatch an emitted event object`() {
        val dispatcher = Dispatcher()

        var passedValue: Event = EmittedEvent("emittedSound")

        dispatcher.eventSubscribe { event -> passedValue = event }

        assertEquals("emittedSound", passedValue.name)
    }

That did the trick and my tests start passing. I do wonder if generics could help out here but I haven't played with them much yet.

How about you? Let me know if you've learned anything interesting about Kotlin's type inference by dropping me a line on the contact page.