1

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)
}
TreffnonX
  • 2,924
  • 15
  • 23
  • @Sweeper It doesn't have to, it's just an unsafe container. Safety is assured by the method accessing it. Besides, it's just an example of what I need. I could really just create `Map, ?>`, or leave out the type arguments altogether, as the user accesses it indirectly anyway. I am just trying to be as conform as I can be. – TreffnonX Dec 28 '21 at 12:12
  • So what's wrong with using `(Any) -> String` if you seem to care so little about this? – Sweeper Dec 28 '21 at 12:14
  • It won't work. A mapper from any to something does not match a mapper from String to String for example. – TreffnonX Dec 28 '21 at 12:15
  • The mappers inside the map are added by a function that accepts only argument class and a mapper from that particular class (or more specific) to String. If I were to store (Any) -> String, that would not accept (String) -> String for example. The mapper would accept String, but the function would not. – TreffnonX Dec 28 '21 at 12:18
  • 1
    Just cast it `as (Any) -> String`. It's an unchecked cast. You are already aware that this is unsafe, so this should not be any surprising. – Sweeper Dec 28 '21 at 12:21
  • This will definetly throw a CCE in the jvm. I don't know if it would run on JS, but my target is JVM anyway. Any Function (or Kotlin function) that takes an argument (String) and maps to String cannot be cast to (Any) -> String. After all you could not cast some function `String toUpperCase(String str)` to be a function `String toUpperCase(Object str)`, because that function would take any argument. – TreffnonX Dec 28 '21 at 12:27
  • 1
    Did you actually verify that it can't be cast? See my answer: I cast `foo(Int): String)` to `(Any) -> String` and this example works properly. – broot Dec 28 '21 at 12:30
  • 1
    It will throw a CCE if you actually pass in a non-`String` to the function, but just casting is fine. Do you know how unchecked casts work? They are ***unchecked*** after all... – Sweeper Dec 28 '21 at 12:30

2 Answers2

2

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.

broot
  • 21,588
  • 3
  • 30
  • 35
  • The map is in fact intended to be used internally only. To access it, a function is used that ensures type safety. I edited the question to correctly use the `?` instead of `*`, which was my mistake. I have not been working with java in a bit. – TreffnonX Dec 28 '21 at 12:03
  • 1
    Haha, same here. I don't use Java recently and I'm starting to be confused with its generics sometimes. Anyway, if we use `?` we won't have to perform unchecked cast when storing the function, but we will have to do it when using the function. So I think both solutions are pretty similar. – broot Dec 28 '21 at 12:20
  • 1
    I updated my answer to remove the information that your Java example is not possible. I can't explain why (and if) we have to implement this a little differently in Kotlin than in Java, but at the end of the day, both solutions are similar and require unsafe casts at some point in time. – broot Dec 28 '21 at 12:28
  • The unsafe cast is perfectly ok, as I said, the access is restricted anyway. I will try your way, but as I mentioned above, I think it will throw a CCE. – TreffnonX Dec 28 '21 at 12:31
  • I am a bit surprised, but it does work. I would have expected it to throw, but it runs. I still don't entirely understand why, but I will look into that. Thank you for your help, anyway! And thank you to @Sweeper as well! – TreffnonX Dec 28 '21 at 12:40
  • In the meantime, I replaced my example with some real data object, because with integers it might look like it just implicitly performs `toString()` somewhere along the way and this is why it worked :-) – broot Dec 28 '21 at 12:44
  • Broot, I've tried to explain [what you couldn't explain](https://stackoverflow.com/questions/70506228/kotlin-equivalent-to-javas-function-string/70507298#comment124634701_70506561). @TreffnonX Hopefully my answer is also helpful. – Sweeper Dec 28 '21 at 12:49
  • @Sweeper Thanks for explanation, but I was rather interested in why in Kotlin we can't have a collection of functions accepting a single and unknown param. But actually... we can - by using `(Nothing) -> String`. At least to me `Nothing` is a little counter-intuitive, but it provides an 1:1 equivalent of Java code. We can implicitly cast `(User) -> String` to `(Nothing) -> String`, but we have to perform unchecked cast when invoking the function passing a `User` to it. – broot Dec 28 '21 at 13:28
  • Oh `Nothing`! That's what Kotlin gives you as the parameter type when you use `out T` on something that should be `in T`! I totally about its existence! – Sweeper Dec 28 '21 at 13:33
  • @broot, It's funny, but I tried `Nothing`before I even posted this question. It does not work, because casting `User` from your example to `Nothing` throws: CacheUnitTest$test simple extractor$SomeClass incompatible with java.lang.Void. This might be one of those instances, where generic stuff compiles perfectly well, but does not run at runtime. – TreffnonX Dec 28 '21 at 13:45
  • @TreffnonX I'm not sure, what exactly did you do, but I tested this before writing my comment - it works for me. If you tried to cast `User` to `Nothing` then honestly, it doesn't make too much sense to me. We have to do it other way around - cast a function from `(Nothing) -> String` to `(User) -> String`. This is unchecked cast and it's an equivalent of casting `Function, String>` to `Function` from the Java solution. https://pl.kotl.in/xHClOahXz But this is just for curiosity, I think `(Any) -> String` approach is a better one. – broot Dec 28 '21 at 20:49
  • Yes, it seems so. I just thought there was a way to cast to `Nothing` but there is not. The cast of the function is always possible that way. But then there is no gain, as written below @Sweeper 's answer. – TreffnonX Dec 28 '21 at 20:55
2

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

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • Actually, the `Nothing` part does not work ;) - you can test this with my example. – TreffnonX Dec 28 '21 at 13:52
  • @TreffnonX Do you mean the part where you do `it.invoke(obj)`? Rather than casting `obj` to `Nothing`, you can cast `it` to `(Any) -> String`, which is unchecked. I still wouldn't use `(Nothing) -> String` though. The type itself is unintuitive IMO. – Sweeper Dec 28 '21 at 13:56
  • That is what I meant, yes. If I have to cast the lambda anyway, then I'd rather do it pre enering it to the map. Otherwise, where is the benefit? If I could do away with casting, that would be another thing, but it does not seem to be possible, – TreffnonX Dec 28 '21 at 14:00
  • @TreffnonX I didn't say there's any benefit :D I just like playing around with types. I've already made it very clear that I don't recommend using `(Nothing) -> String`, but it is the closest to `Function, String>`, in terms of variance. – Sweeper Dec 28 '21 at 14:04