Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I tell kotlin that a function doesn't return null if the parameter is not null?

I want to write a convenience extension to extract values from a Map while parsing them at the same time. If the parsing fails, the function should return a default value. This all works fine, but I want to tell the Kotlin compiler that when the default value is not null, the result won't be null either. I could to this in Java through the @Contract annotation, but it seems to not work in Kotlin. Can this be done? Do contracts not work for extension functions? Here is the kotlin attempt:

import org.jetbrains.annotations.Contract

private const val TAG = "ParseExtensions"

@Contract("_, !null -> !null")
fun Map<String, String>.optLong(key: String, default: Long?): Long? {
    val value = get(key)
    value ?: return default

    return try {
        java.lang.Long.valueOf(value)
    } catch (e: NumberFormatException) {
        Log.e(TAG, e)
        Log.d(TAG, "Couldn't convert $value to long for key $key")

        default
    }
}

fun test() {
    val a = HashMap<String, String>()

    val something: Long = a.optLong("somekey", 1)
}

In the above code, the IDE will highlight an error in the assignment to something despite optLong being called with a non null default value of 1. For comparison, here is similar code which tests nullability through annotations and contracts in Java:

public class StackoverflowQuestion
{
    @Contract("_, !null -> !null")
    static @Nullable Long getLong(@NonNull String key, @Nullable Long def)
    {
        // Just for testing, no real code here.
        return 0L;
    }

    static void testNull(@NonNull Long value) {
    }

    static void test()
    {
        final Long something = getLong("somekey", 1L);
        testNull(something);
    }
}

The above code doesn't show any error. Only when the @Contract annotation is removed will the IDE warn about the call to testNull() with a potentially null value.

like image 426
Grzegorz Adam Hankiewicz Avatar asked Apr 13 '18 12:04

Grzegorz Adam Hankiewicz


2 Answers

You can do this by making the function generic.

fun <T: Long?> Map<String, String>.optLong(key: String, default: T): T 
{
    // do something.
    return default
}

Which can be used like this:

fun main(args: Array<String>) {
    val nullable: Long? = 0L
    val notNullable: Long = 0L

    someMap.optLong(nullable) // Returns type `Long?`
    someMap.optLong(notNullable) // Returns type `Long`
}

This works because Long? is a supertype of Long. The type will normally be inferred in order to return a nullable or non-nullable type based on the parameters.

This will "tell the Kotlin compiler that when the default value is not null, the result won't be null either."

like image 53
Aro Avatar answered Sep 30 '22 08:09

Aro


It's a pity that you can't do this, in Kotlin 1.2 or below.

However, Kotlin is working on contract dsl which is unannounced yet, which is not available ATM (since they're declared internal in the stdlib) but you can use some hacks to use them in your codes (by compiling a stdlib yourself, make all of them public).

You can see them in the stdlib ATM:

@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

Maybe there will be something like

contract {
   when(null != default) implies (returnValue != null)
}

in the future that can solve your problem.

Workaround

Personally I'd recommend you to replace default's type with a NotNull Long and call it like

val nullableLong = blabla
val result = nullableLong?.let { oraora.optLong(mudamuda, it) }

result is Long? and it's null only when nullableLong is null.

like image 23
ice1000 Avatar answered Sep 30 '22 08:09

ice1000