Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin out-projected type prohibits the use

I recently tried the following in Kotlin. The idea is that I will receive as input an Item (like AmericanItem for instance) that extends BaseItem. I am trying to have a different parser for each of theses items Here is a sample code

abstract class BaseItem
class AmericanItem : BaseItem()
class EuropeanItem : BaseItem()

interface ItemParser<T : BaseItem> {
    fun parse(item: T)
}

class AmericanItemParser : ItemParser<AmericanItem> {
    override fun parse(item: AmericanItem) {
        println("AmericanItemParser")
    }
}

class EuropeanItemParser : ItemParser<EuropeanItem> {
    override fun parse(item: EuropeanItem) {
        println("parsing EuropeanItem")
    }
}

fun main(args: Array<String>) {
    val hashMap = HashMap<Class<out BaseItem>, ItemParser<*>>()
    hashMap.put(AmericanItem::class.java, EuropeanItemParser())
    hashMap.put(EuropeanItem::class.java, AmericanItemParser())

    val inputItem = EuropeanItem()
    val foundParser = hashMap[inputItem.javaClass]
    foundParser?.parse(inputItem)
}

My issue is at the very last line, when I am trying to invoke the parser, I am getting the following compilation error

Out-projected type 'ItemParser<*>?' prohibits the use of 'public abstract fun parse(item: T): kotlin.Unit defined in ItemParser'

What am I doing wrong here ?

like image 789
Damien Locque Avatar asked Nov 01 '18 00:11

Damien Locque


1 Answers

You have created a conflict between your declarations of the Map and of the ItemParser. The map can contain any descendant of BaseItem but the ItemParser is designed that each descendant only operates on one of the descendants of BaseItem. So for a given instance of ItemParser it must accept something it can recognize and here you can't do that because your foundParser could be any descendant and not the one true expected type for that given ItemParser instance. Which T should it guess at?!? It cannot.

Therefore you have to design your API around the base class and not the descendants. You make it impossible for the compiler to know what is be passed to the parse() method. The only one true thing you can know is that it is a BaseItem instance.

Only you know the trick you are doing with the map that guarantees you are calling the correct instance with the correct type. The compiler has no idea of your logic that makes that a guarantee.

I would suggest you change your API to add an internalParse method for which you cast do your work, wrapped by a generic parse function that double checks and does the evil cast.

abstract class BaseItem

class AmericanItem : BaseItem()
class EuropeanItem : BaseItem()

interface ItemParser<T: BaseItem> {
    @Suppress("UNCHECKED_CAST")
    fun parse(item: BaseItem) {
        val tempItem = item as? T 
             ?: throw IllegalArgumentException("Invalid type ${item.javaClass.name} passed to this parser")
        internalParse(tempItem)
    }

    fun internalParse(item: T)
}

class AmericanItemParser : ItemParser<AmericanItem> {
    override fun internalParse(item: AmericanItem) {
        println("AmericanItemParser")
    }
}

class EuropeanItemParser : ItemParser<EuropeanItem> {
    override fun internalParse(item: EuropeanItem) {
        println("parsing EuropeanItem")
    }
}

fun main(args: Array<String>) {
    val hashMap = HashMap<Class<out BaseItem>, ItemParser<*>>()
    hashMap.put(AmericanItem::class.java, EuropeanItemParser())
    hashMap.put(EuropeanItem::class.java, AmericanItemParser())

    val inputItem = EuropeanItem()
    val foundParser = hashMap[inputItem.javaClass]
    foundParser?.parse(inputItem)
}

Note you could also use the Kotlin class instead of the Java class which would be of type KClass<out T>.

like image 53
Jayson Minard Avatar answered Oct 13 '22 05:10

Jayson Minard