3
votes

Kotlin newby here. I have a set of functions that accept and parse different inputs (plain text, json, xml) but that have the same output (and instance of Event). The code looks as follows (complete version at https://pastebin.com/UNJFGZsm):

    data class Event(val id: Int)
    val stringToEvent: (String) -> Event = { s -> Event(s.toInt()) }
    val dummyToEvent: (Document) -> Event = { _ -> Event(1) }
    val jsonToEvent: (JsonNode) -> Event = { j -> Event(j.get("id").asInt()) }
    fun elementGen(opt: String): Any {
        // return a String, or a JsonNode, or a Document
        // ...
    }
    fun main(args : Array) {

        val parser = when (args[0]) {
            "string" -> stringToEvent  // it builds if I remove this line
            "json" -> jsonToEvent
            "xml" -> dummyToEvent
            else -> throw RuntimeException("Option not supported")
        }

        print(parser(elementGen(args[0])))
    }

When I try to build I get the error that follows:

(44, 11): Out-projected type 'Function1<*, Event>' prohibits the use of 'public abstract operator fun invoke(p1: P1): R defined in kotlin.Function1

However, the code seems to build and work correctly if I don't use the stringToEvent function.

Why is that? Why does the problem only seem to effect the (String) -> Event type function?

1

1 Answers

2
votes

The reason is that the input type of the function is * (i.e., it has constraints but the constraints are unknown). A function with a star-projection input type cannot be called. From the Kotlin generics page:

For Foo<in T>, where T is a contravariant type parameter, Foo<*> is equivalent to Foo<in Nothing>. It means there is nothing you can write to Foo<*> in a safe way when T is unknown.

The type of the provided argument (elementGen(args[0])) is Any. This is because the union of the possible parameter types from the when clause has no common type beneath Any. Thus, elementGen(args[0]) is an invalid argument for the function.

The weird thing it seems is that although kotlinc is able to detect this error in your original code, it fails to detect it when the String input type is removed. It's weird that compilation succeeds in that case. Given that the argument type (Any) does not satisfy the input type (the intersection type Document & JsonNode, which is still equivalent to *, just like Document & JsonNode & String), I would expect this to fail. In fact, you can even see that type-safety is lost if you change output types of elementGen so that they do not match the corresponding functions for parser. Try this, for example:

fun elementGen(opt: String): Any {
    return when (opt) {
        "string" -> "1"
        "json" -> {
            "1"
//            val mapper = ObjectMapper()
//            mapper.readTree("{\"id\": 1 }")
        }
        "xml" -> {
            DocumentBuilderFactory.newInstance().newDocumentBuilder().parse("<id>1</id>")
        }
        else -> throw RuntimeException("Option not supported")
    }
}

fun main(args: Array<String>) {

    val parser = when (args[0]) {
//        "string" -> stringToEvent
        "json" -> jsonToEvent
        "xml" -> dummyToEvent
        else -> throw RuntimeException("Option not supported")
    }
    print(parser(elementGen(args[0])))
}

You can see that compilation still succeeds, but a class cast exception is raised at runtime:

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to com.fasterxml.jackson.databind.JsonNode
    at com.example.demo.config.TestKt$jsonToEvent$1.invoke(test.kt)
    at com.example.demo.config.TestKt.main(test.kt:39)

I think you may have found a bug. The issue tracker is here if you choose to add a ticket.