Type inference in lambda default argument

My simplified code looks like this:

fun <T> foo(x: Int = 1, conv: (Int) -> T = { it }) = conv(x)

The compiler issues a “Type Mismatch” (T vs. Int).
However, the following code works fine:

fun <T> foo(x: Int = 1, conv: (Int) -> T) = conv(x)
foo(1) { it }
foo { it }

Why does the type inference for the default argument fail while it works perfectly when actually used?


In the function declaration, the compiler has no way of knowing the type of T. When calling it, you can give it context. I believe you could fix this by giving T a bound, if you plan on only using it for numbers

fun <T : Number> foo(x: Int = 1, conv: (Int) -> T) = conv(x)

I would like to give conv the default argument { it } which seems to be impossible in your case as well.
Also, { it } does not need further context right?

In my imagination, default arguments are conceptually evaluated/copied into function when it is called and thus { it } should properly work.

The compiler is not able to know how to convert “it”(Int) to T. Imagine if you call this function that way:

val b: Bar = foo(2)

This might work but will give you exception at runtime if you try the previous line:

fun <T> foo(x: Int = 1, conv: (Int) -> T = { it as T }) = conv(x)

I think the problem is you might do this:


foo<String> requires a lambda that returns String, but it’s getting one that returns Int.

fun <T> foo(x: Int = 1, conv: (Int) -> T = { it as T }) = conv(x)

This works, but it looks dangerous. the it as T won’t actually check if the value is of type T.
And now you can’t call foo without type arguments.

if I try to call



val b: Bar = foo(1)

then the compiler could (easily) mark this as an error with the same reason why

val b: Bar = foo(1) { it }

would cause it.

Thanks for you both to suggest

fun <T> foo(x: Int = 1, conv: (Int) -> T = { it as T }) = conv(x)

as a workaround. This looks smelly and IDEA warns due to the unchecked cast and I would rather not get runtime exceptions. Btw, why is reified not needed here? I thought the type was erased.

The type is erased :slight_smile: that’s why it doesn’t actually throw an exception.

println(foo<String>(42)) // prints 42
println(foo<String>(42).length) // throws ClassCassException

You can use this, as a workaround, if you know T will always be a Number:

inline fun <reified T: Number> foo(x: Int = 1, noinline conv: (Int) -> T = { it as T }) = conv(x)

In that case reified makes the cast safe.

@Eliote that does unfortunately not work due to the same issue as Lambda parameter with default parameter in inline functions

Have you considered adding an overloaded function?
fun foo(x: Int = 1) = x
since all you’re trying to do is to cover a specific case with Int as your T type which doesn’t require a function to be generic

@Stas.Shusha Yeah that is the solution that I currently use. I have several of those functions, so I ended up with quite some code duplication that I would have loved to avoid using inline lambdas.

This post seems to be related: Nulls and generics - #7 by ilya.gorbunov

An idea was proposed there that default parameter value could add some additional constraints to the parameter type, but these constraints would be only honored by the compiler if the actual value for that parameter is missing.

For example in this function declaration:
fun <T> foo(x: Int = 1, conv: (Int) -> T = { it })
the default value of conv could add a constraint T >: Int (T should be supertype of Int), but that constraint would be ignored if you specify the value of conv when calling the function.

And if you omit the argument, the constraint could help to report an error about the invalid actual type parameter:

foo<String>(1) // ERROR: String is not supertype of Int
foo(1, { it.toString() }) // returns "1"
foo<Int>(1) // returns 1
foo(1) // returns 1
1 Like

That sounds delicious and exactly what my intuition would prefer.

I’ll keep my fingers crossed that this has no hidden downsides and doesn’t unproportionally blow up the compiler implementation complexity :slight_smile: