Szymon Jeziorski
09/11/2023, 7:29 AMMap.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:
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`:
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?Ronny Bräunlich
09/11/2023, 7:37 AMremappingFunction
is V
, not V?
. Therefore the compiler errorSzymon Jeziorski
09/11/2023, 7:39 AMV : Any
so I would expect V
to also accept nullable typesRonny Bräunlich
09/11/2023, 7:42 AMAny?
. There is a difference between Any
and Any?
. See https://miro.medium.com/v2/resize:fit:720/format:webp/1*EfanxY8YrEg4vyq8u8iz7Q.png▾
Szymon Jeziorski
09/11/2023, 7:49 AMAny
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>
Ronny Bräunlich
09/11/2023, 7:56 AMSzymon Jeziorski
09/11/2023, 7:59 AMinline 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)
@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
Ronny Bräunlich
09/11/2023, 8:10 AMinline 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 } }
Szymon Jeziorski
09/11/2023, 8:21 AMnull
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 inferredRonny Bräunlich
09/11/2023, 8:27 AMV
. 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 aboveSzymon Jeziorski
09/11/2023, 8:36 AMSzymon Jeziorski
09/11/2023, 9:34 AMWout Werkman
09/11/2023, 10:09 AMV
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
.
inline fun <K : Any, V: Any> MutableMap<K, V>.merge(
key: K,
value: V,
remappingFunction: (oldValue: V) -> V?
): V?
Szymon Jeziorski
09/11/2023, 10:18 AM@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 APISzymon Jeziorski
09/11/2023, 11:05 AMType 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)
inline fun <K : Any, V : R & Any, R> MutableMap<K, V>.merge(
key: K,
value: V,
remappingFunction: (oldValue: V) -> R,
): R
V
and R
can be resolved individually. Yay!