I have a function:
object CliUtils {
var skipOptionalArgs = false
fun readOptional(variableName: String, defaultValue: String? = null): String? {
if (skipOptionalArgs) return defaultValue
print("Provide the $variableName (optional${if (defaultValue != null) ", default: $defaultValue" else ""}): ")
return readln().takeIf { it.isNotEmpty() } ?: defaultValue
}
}
When I use this function like so...
val inputName = readOptional("input name", "Input")
...the the type of inputName
is String?
. Due to the defaultValue
of "Input"
that I gave though, I know that the variable is not null
.
I could use a double exclamation mark like so:
val inputName = readOptional("input name", "Input")!!
But I feel that there should be a way to define the readOptional
function so that this return type of String could be inferred, without the use of double exclamation mark. I thought of contracts, but to my knowledge contracts can only refer to parameters, not to the return type. I also thought of generics to link the type of the defaultValue
parameter type to the return type, but I couldn't get that to work with its nullability.
Is there any way to make kotlin understand that if defaultValue != null
that the return value also isn't null?
It's possible with generics:
@Suppress("UNCHECKED_CAST")
fun <T : String?> readOptional(variableName: String, defaultValue: T = null as T): T {
if (skipOptionalArgs) return defaultValue
print("Provide the $variableName (optional${if (defaultValue != null) ", default: $defaultValue" else ""}): ")
return (readln().takeIf { it.isNotEmpty() } ?: defaultValue) as T
}
The caller side doesn't need the evil !!
check anymore:
val inputName: String = readOptional("input name", "Input")
val inputNameNullable: String? = readOptional("input name", null)
val inputNameDefaultNullable: String? = readOptional("input name")
But the price we have to pay is the unchecked cast. This is, because the compiler cannot ensure that the method returns the not null default paramater in every condition.
So we made it comfortable for the caller site to use all variants without the additional check. But as mentioned the compiler can't prevent us in using it wrong. It's stillt possible to additional set the (wrong) generic type or let it infer the wrong from the val type:
val inputNameDefaultTypedWrong: String = readOptional<String>("input name")
val inputNameDefaultNullableWrongInferred: String = readOptional("input name")
Edit: This can be also solved in combination with an additional method overload, like mentioned by @Tenfour04:
fun readOptional(variableName: String): String? = readOptional(variableName, null)
My idea and more explanations can be also found here: Kotlin generics with nullable types