Home > database >  Generic producer and consumer without exposing concrete types in Kotlin
Generic producer and consumer without exposing concrete types in Kotlin

Time:07-11

I've been reading up on Kotlin generics for a while and came to a sense that it's a very powerful feature of the language.

This made me wonder and experiment, and I can't figure out a nice solution to the following problem. Say I have the following code:

interface WidgetProducerConsumer<T> {
    fun produce(): Widget<T>
    fun consume(widget: Widget<T>)

    companion object {
        fun getStringWidgetProducerConsumer(): WidgetProducerConsumer<*> {
            return StringWidgetProducerConsumer()
        }
    }
}

data class Widget<T>(val contents: T)

private class StringWidgetProducerConsumer: WidgetProducerConsumer<String> {
    override fun produce(): Widget<String> {
        return Widget("hello world")
    }

    override fun consume(widget: Widget<String>) {
        // Do something with string here, since we know it can only be a sting
        println(widget.contents)
    }
}

And in another part of my project I wanted to do something like:

val producerConsumer = WidgetProducerConsumer.getStringWidgetProducerConsumer()

// Can't do this with WidgetProducerConsumer<*> because the consumed type is Nothing
// and returned type is Any?
producerConsumer.consume(producerConsumer.produce())

I would naturally not be able to, since at the use site the star projection turns the T into their highest Any? and lowest Nothing bounds as it is intended to do.

I want to make sure that the implementation of WidgetProducerConsumer takes only Widgets that it itself produces. This would allow me to inject specific implementations of the WidgetProducerConsumer without exposing information about the internal minutiae to the consumer. Is there a pattern or recipe for it?

Intuitively it feels like there should be a nice way to achieve this without resorting to type checks, casting and alike, however I can't figure it out.

CodePudding user response:

Using the star-projected type with <*> indeed limits you this way.

The obvious fix here is simply to avoid the unnecessary projection and just make getStringWidgetProducerConsumer() return the more precise type WidgetProducerConsumer<String>:

companion object {
    fun getStringWidgetProducerConsumer(): WidgetProducerConsumer<String> {
        return StringWidgetProducerConsumer()
    }
}

However, assuming you don't have a choice and you already have a WidgetProducerConsumer<*> from somewhere you don't control, you should still theoretically be able to use the fact that you're using the same producerConsumer variable and the type should match. This is actually possible if you simply extract the piece of code into a generic function, so you can use a proper type instead of *:

fun <T> WidgetProducerConsumer<T>.produceAndConsume() {
    consume(produce())
}

// and then you can do
val producerConsumer = WidgetProducerConsumer.getStringWidgetProducerConsumer()

producerConsumer.produceAndConsume()

Here, produceAndConsume() defines a type T as being the one from WidgetProducerConsumer<T>, and then can use T normally. You don't really get more information out of the star-projected type (that would not be possible), but you just define the type clearly for the compiler. It's like in math saying "let T be the * in WidgetProducerConsumer<*>" so you can then do things with it.

  • Related