Hi, I wanted to write Kotlin versions of Java's `M...
# getting-started
s
Hi, I wanted to write Kotlin versions of Java's
Map.merge
,
Map.compute
functions inferring result type nullability from
remappingFunction
lambda, to avoid redundant nullability checks (using Java's API results in nullable return type in all cases). To achieve that, I thought of leveraging definitely non-nullable types and came up with the following signature:
Copy code
inline fun <K : Any, V> MutableMap<K, V & Any>.merge(
    key: K,
    value: V & Any,
    remappingFunction: (oldValue: V & Any) -> V
): V
Basically I wanted function to only accept non-nullable
value
, but at the same time I wanted to allow
remappingFunction
to return nullable type and also use the return type to infer generic parameter being returned from function. My problem is that in case of nullable lambda return type, the compiler doesn't infer it but rather fails with `Type mismatch`:
Copy code
val map: MutableMap<String, Int> = mutableMapOf()
val newValue = 5

// correctly infers Int type
val nonNullableResult = map.merge(key, newValue) { old -> old * newValue }

// I would expect it to infer Int? type, but compiler states lambda return type mismatch, expected Int, found Int?
val nullableResult = map.merge(key, newValue) { old -> (old * newValue).takeIf { it > 5 } }
Am I doing something wrong? How could I fix the signature to achieve what I want?
r
Your function signature states that the return value of
remappingFunction
is
V
, not
V?
. Therefore the compiler error
s
Yes, but I do not restrict
V : Any
so I would expect
V
to also accept nullable types
r
You gotta use
Any?
. There is a difference between
Any
and
Any?
. See

https://miro.medium.com/v2/resize:fit:720/format:webp/1*EfanxY8YrEg4vyq8u8iz7Q.png

s
I understand the difference between
Any
and
Any?
and basic Kotlin type system but I don't see how using
Any?
anywhere here would resolve my issue. Unbounded generic parameter resolves to
Any?
by default, using
fun <K : Any, V : Any?>
would be equivalent to
fun <K: Any, V>
r
Can you show me your merge implementation?
s
Sure, here's what I had so far: For MutableMap:
Copy code
inline fun <K : Any, V> MutableMap<K, V & Any>.merge(
    key: K,
    value: V & Any,
    remappingFunction: (oldValue: V & Any) -> V
): V = when (val oldValue = get(key)) {
    null -> value.also { put(key, it) }
    else -> remappingFunction(oldValue).also { newValue ->
        if (newValue != null) put(key, newValue) else remove(key)
    }
}
For ConcurrentMap: (calling original function to preserve atomicity, unchecked cast due to fixed Java nullability)
Copy code
@Suppress("UNCHECKED_CAST")
inline fun <K : Any, V> ConcurrentMap<K, V & Any>.merge(
    key: K,
    value: V & Any,
    crossinline remappingFunction: (oldValue: V & Any) -> V
): V = merge(key, value) { oldValue, _ -> remappingFunction(oldValue) } as V
r
So, what compiles for me is:
Copy code
inline fun <K : Any, V> MutableMap<K, V & Any>.merge(
    key: K,
    value: V & Any,
    remappingFunction: (oldValue: V & Any) -> V?
): V = get(key)?.let { oldValue ->
    remappingFunction(oldValue)
        .also { newValue -> if (newValue != null) put(key, newValue) else remove(key) }
} ?: value.also { put(key, it) }

val map: MutableMap<String, Int> = mutableMapOf()
val newValue = 5

// correctly infers Int type
val nonNullableResult: Int = map.merge("key", newValue) { old -> old * newValue }

// I would expect it to infer Int? type, but compiler states lambda return type mismatch, expected Int, found Int?
val nullableResult: Int = map.merge("key", newValue) { old -> (old * newValue).takeIf { it > 5 } }
s
Sorry, I pasted wrong scratch implementation which indeed returned non-nullable type, due to elvis operator mapping
null
returned from
remappingFunction
. Fixed and edited my previous response. I don't want
nullableResult
to be non-nullable. I want its return type to be inferred from passed lambda expression. I want my extensions to behave as original Java's
Map.merge
and
Map.compute
methods - to return new value if it is present or else return
null
- for my merge function
null
can be returned only if lambda returns
null
, that's why I would like the nullability to be inferred
r
From my point of view you cannot infer the nullability from
V
. If you create a map with non-nullable Values but you want your
merge
function to be able to return null, then you have to return
V?
. In this case, in your example, you'll always have
Int?
as return type. This looks correct to me because that's what you described above
s
Oh okay, I thought definitely non-nullable types from Kotlin 1.7 described here could help me to achieve that, but maybe I was wrong about it. Anyway, thanks for your effort!
w
I agree that you might expect the compiler to infer
V
to be
Int?
from the closure. But I'm quite sure they infer it via the non-closure argument, since it's so much cheaper than inferring via closures. For me it seems like you can make
V: Any
, then replace
V
with
V?
and
V & Any
with
V
.
Copy code
inline fun <K : Any, V: Any> MutableMap<K, V>.merge(
  key: K,
  value: V,
  remappingFunction: (oldValue: V) -> V?
): V?
s
I guess inferring from non-closure would make sense if it's cheaper. I also experimented with using different signatures (nullable + non-nullable) along with
@OverloadResolutionByLambdaReturnType
annotation, but it didn't work. I could make return type
V?
as you suggested but that unfortunately would defeat my original purpose of the function having inferred returned type instead of fixed nullable return type from Java's API
I thought I got it, then stumbled on
Type mismatch. Required: V, Found: R & Any
within function's body. (IntelliJ automatically imported
put
as static import from some library and it looked like everything was fine lol) After some trial and error I got it working with following signature:
Copy code
inline fun <K : Any, V : R & Any, R> MutableMap<K, V>.merge(
    key: K,
    value: V,
    remappingFunction: (oldValue: V) -> R,
): R
I guess it makes sense for it to work with since
V
and
R
can be resolved individually. Yay!
🙂
sad panda 1