7

In Kotlin, I'm trying to compile the following:

  1. given an interface with generic types (Printer)
  2. and two implementations of that interface (DogPrinter and CatPrinter)
  3. return one of the printers, according to an external variable (AnimalType)

The following code does not compile, because a type is required at: fun getMapper(animalType: AnimalType): Printer

I tried to use <Any> or <*> but got no success. Can someone help?

(easy to see the error by copypasting the code below into https://try.kotlinlang.org)

enum class AnimalType {
    CAT, DOG
}

class Dog
class Cat

interface Printer<in T> {
    fun mapToString(input: T): String
}


class DogPrinter : Printer<Dog> {
    override fun mapToString(input: Dog): String {
        return "dog"
    }
}

class CatPrinter : Printer<Cat> {
    override fun mapToString(input: Cat): String {
        return "cat"
    }
}

private fun getMapper(animalType: AnimalType): Printer {
    return when(animalType) {
        AnimalType.CAT -> CatPrinter()
        AnimalType.DOG -> DogPrinter()
    }
}

fun usage_does_not_compile() {
    getMapper(AnimalType.DOG)
            .mapToString(5)
}
Ricardo Belchior
  • 576
  • 1
  • 3
  • 14
  • Is it necessary to have an enum which seems to do nothing more than mirror the class types? You could simply be passing in the type to getMapper rather than trying to match enums to types. – dillius Oct 25 '17 at 17:15
  • it was meant as an example, since in my real case i preferred to have an enum/identifier. But in the end yes, i could pass the type – Ricardo Belchior Oct 26 '17 at 22:15

3 Answers3

6

I've modified your code a bit. The getMapper function is inline with a reified generic type now, which makes the call pretty readable at getMapper<Dog>().

Read about reified here: What does the reified keyword in Kotlin really do?

private inline fun <reified T> getMapper(): Printer<T> {
    when (T::class) {
        Cat::class -> return CatPrinter() as Printer<T>
        Dog::class -> return DogPrinter() as Printer<T>
    }
    throw IllegalArgumentException()
}

fun main(args: Array<String>) {
    println(getMapper<Dog>().mapToString(Dog()))
    println(getMapper<Cat>().mapToString(Cat()))
}

The reified thing is not even necessary actually but makes the client side more readable. Alternatively, you could also pass in the class as an argument to the getMapper function. The really important part is to make this one generic. The unchecked casts are not very cool but seem to be safe here.

s1m0nw1
  • 76,759
  • 17
  • 167
  • 196
  • i don't understand why we would need the "as Printer" inside getMapper, but that's ok. Your answer was the best, thanks. I ended up using a solution where i pass in the class type as a getMapper argument. (copied from your link) – Ricardo Belchior Oct 26 '17 at 22:18
  • In the end this looks like an example for "How to break type safety?". – ceving Nov 18 '22 at 09:03
0

Kotlin doesn't have raw types so you can't return Printer from getMapper. You can either return Printer<*>, make getMapper generic, or change your inheritance structure to have a common super type.

Ruckus T-Boom
  • 4,566
  • 1
  • 28
  • 42
0

Returning Printer<*> works fine. But it isn't actually useful because you can't call mapToString(Dog()) (I assume mapToString(5) is a typo) and it's a good thing: if it compiled then

getMapper(AnimalType.CAT).mapToString(Dog())

would also have to compile, because the return type can only depend on argument type, not on value, and AnimalType.CAT and AnimalType.DOG have the same type. There are some languages which allow it, but Kotlin isn't one of them.

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • yes, mapToString(5) was a typo. Anyway, i had to fight with the compiler to understand the error messages but now that you say that, it makes sense. thanks for your answer. – Ricardo Belchior Oct 26 '17 at 22:21