Mildly surprised that this compiles fine: ```fun ...
# getting-started
r
Mildly surprised that this compiles fine:
Copy code
fun <T : CharSequence> foo(input: T): Pair<T, Int> = input to input.length
fun foo(): Pair<String, Int> = foo("defaultvalue")
but switching it to a default argument does not:
Copy code
// does not compile, Type mismatch. Required: T Found: String
fun <T : CharSequence> foo(input: T = "defaultvalue"): Pair<T, Int> = input to input.length
Intuitively I would expect it to work.
y
foo<StringBuilder>().first
is expected to magically create a string builder out of nowhere
r
foo<StringBuilder>().first
is a compilation error.
y
I mean in the second version, that would compile. For instance,
Copy code
fun <T : CharSequence> foo(input: T = TODO()): Pair<T, Int> = input to input.length
r
I'm suggesting it should not in the second version, because the compiler can tell that
StringBuilder
and
String
are not compatible subtypes of
CharSequence
, so it cannot resolve
T
.
y
I understand, but the compiler doesn't really build a connection between the default argument and the type parameter. Keep in mind that something like this would also be allowed:
Copy code
inline fun <reified T : CharSequence> foo(input: T = "defaultvalue"): Pair<T, Int> = magicallyCreate<T>() to input.length
So how's the compiler to know that explicitly specifying the type parameter is not allowed if the default value is used?
r
Surely explicitly specifying the type parameter is allowed, but has to be the type (or supertype) of the default value? And if it isn't, that's a compile error?
y
Another way to look at it is that the second is equivalent to:
Copy code
fun <T : CharSequence> foo(input: T? = null): Pair<T, Int> {
  val input: T = input ?: "defaultvalue"
  return input to input.length
}
And of course
String
is not automatically
T
.
r
But that's not what's happening. Default values are inlined at the call site, where it's perfectly legitimate to pass a String.
y
The default value doesn't induce any type bounds on
T
on its own. What you're asking for is a lower bound on
T
, which doesn't even exist in Kotlin. Default values actually compile to something sort-of similar to what I showed. They're not macro-inlined or anything like that.
Another way to look at it is sure, they're inlined at the call site, but you get an error because there's some call site (namely
foo<StringBuilder>()
) where it'd fail. I guess the error could be somehow propagated to the call site, but I can imagine some situations where that'd be undesirable. Also, the type of the default value is not part of the API signature of a function. The fact that it has default values is, but the type of a default value itself isn't. Supporting this would mean that changing the type of a default value results in binary-incompatible changes, which doesn't quite fit the mental model for default values
k
The following compiles successfully, albeit a bit hacky:
Copy code
fun <T : CharSequence> foo(
    @Suppress("UNCHECKED_CAST") input: T = "defaultvalue" as T
): Pair<T, Int> = input to input.length
Also, you need to specify a parameter type at the call site, such as
foo<String>()
r
I don't mind overloading the function, I think the overloading in this case is a lot nicer than casting and requiring a parameter type at call sight. I was just surprised; I expected referential transparency between
fun foo(arg: Baz) = TODO(); fun foo() = foo(defaultValue)
and
fun foo(arg: Baz = defaultValue) = TODO()
.
y
That referential transparency does exist, but while preserving the type parameters:
Copy code
fun <T : CharSequence> foo(input: T): Pair<T, Int> = input to input.length
fun <T : CharSequence> foo(): Pair<T, Int> = foo("defaultvalue") // doesn't compile
Maybe that clears it up a bit more? The type parameters are considered part of the signature that needs to be preserved, and they're not influenced by default values at all
r
I suppose so.