Home > Software engineering >  Kotlin equivalent to java's Function<*, String>
Kotlin equivalent to java's Function<*, String>

Time:12-28

I have a use case where I need to create a map mapping from KClass to a (lambda) function that converts instances of that class to something else (in my case a String). In java, I would write something along the lines of this:

private Map<Class<?>, Function<?, String>> mappers = new HashMap<>();

In Kotlin, the syntax for a lambda parameter should be (Something...) -> ReturnType, but this does not accept either *, nor in or out. Note, that the map should be able to contain any mapper taking any argument that is of the specified type or more specific in order to be most lenient (however since this is dynamic, it will not be validated. It is just important to know for casting purposes).

How is this expressed correctly?

How I solved it in the end

Thanks to @broot and @Sweeper, I was able to implement my use case. Because I had some other issues, and I assume that anyone who finds this has a similar use case, I want to add the rest of the code in question. I ultimately went with the following (reduced to it's essentials):

// note that this map is public because it is accessed by a public 
// inline function. Making it private won't compile.
val mappers = HashMap<KClass<*>, (Any) -> String>()

// this is used to add mappers to the map of mappers.
// note the noinline!
inline fun <reified T> mapping (noinline mapper: (T) -> String) {
    mappers[T::class] = mapper as (Any) -> String
}

// This is the only method accessing the map to extract mappers and 
// directly use them.
fun mapToString(obj: Any): String {
    // some stuff here...

    // Attempt to map to the id via a predefined mapper.
    candidate = mappers[obj::class]?.let { it.invoke(obj) }
    if (candidate != null) return candidate

    // some other fallback here...
}

Also note that all of the above is nested within another class which I will for the sake of argument call Cache. This is the unit test:

@Test
fun `test simple extractor`() {
    class SomeClass(val somethingToExtract: String)
    val someInstance = SomeClass("someValue")
    
    val cache = Cache()
    // defining the extractor
    cache.mapping<SomeClass> { it.somethingToExtract }

    // using the extractor
    val id = cache.mapToString(someInstance)
    assertEquals(id, someInstance.somethingToExtract)
}

CodePudding user response:

This kind of operation is by default disallowed in both Java and Kotlin, because it is not type-safe. The problem is that you can take for example a function receiving an integer, store in the map and then use it later passing a string to it.

We can force the compiler to disable type guarantees by performing unchecked casts. We need to use Any instead of *:

private val mappers = mutableMapOf<KClass<*>, (Any) -> String>()

fun main() {
    mappers[User::class] = ::getUserName as (Any) -> String // unchecked cast

    val john = User("John")
    val username = mappers[User::class]?.invoke(john)
    println(username) // John
}

fun getUserName(user: User): String = user.name

data class User(val name: String)

Then we have to make sure that types are used correctly. Probably the best would be to wrap this map in our own utility that performs runtime checks on types in order to provide type-safety.

CodePudding user response:

Your Java's type is written a little weirdly. In particular, the type of the function is:

Function<? extends Object, String>

Note that ? is the same as ? extends Object. This breaks PECS, and you wouldn't be able to safely pass anything except null to its apply method.

The first type parameter of Function is the type that the function accepts (consumes), which according to PECS, should be marked with super (contravariant), not extends (covariant):

Function<? super Object, String>

If you follow PECS for the second type parameter too, it would be Function<? super Object, ? extends String>.

Now, Kotlin doesn't let you break PECS in the first place. Every function type in Kotlin automatically has contravariant parameter types and covariant return types. This is why you can assign a (Any) -> String to a (String) -> Any without any casts.

The Kotlin function type equivalent of Function<? super Object, ? extends String> is:

(Any) -> String

or (Any?) -> String depending on how much you like nullables.

You should make your map have one of those types as the value type, and since this changes the variance, you will need to change where you do the unchecked casts, but that should be straightforward.

As broot reminded me in the comments, Kotlin has a Nothing type which Java doesn't. This is the subtype of all types (compare that to Any - the supertype of all types). You can see this when you try to break PECS in Kotlin, e.g. trying to call apply on a Function<*, String>, you will see that apply takes a Nothing.

Therefore, you could write (Nothing) -> String to represent Function<?, String>, but I don't recommend doing this. "A function that takes 'nothing' as a parameter" is just a bit too hard to read and confusing. Does it take a parameter or not? :D

  • Related