Hi everyone! I have a question regarding type infe...
# compiler
m
Hi everyone! I have a question regarding type inference of a lamda. Why does compiler infers different type depends of whether a lambda is used as an argument or initialize a variable? Is this the expected behavior? This thing also breaks the inline refactoring making it a changing semantic one.
Copy code
fun <S> create(code: () -> S): S {
    println("string")
    return code()
}

fun create(code: () -> Unit) {
    println("unit")
}

fun main() {
    create { "a" } // prints unit
    val a = { "a" }
    create(a) // prints string
}
j
this is more apples-to-apples but doesn't change the outcome.
Copy code
fun <S> create(code: () -> S): S {
println("string")
return code()
}

fun <S:Unit>create(code: () -> S):S {
println("unit")
}

fun main( ) {
create { "a" } // prints unit
val a = { "a" }
create(a) // prints string
}
e
Kotlin type inference is limited to statements/expressions, so it makes sense that
Copy code
val a = { "a" }
is inferred to be
Copy code
val a: () -> String = { "a" }
which then means that only
create<String>
is callable
so the real question is, why does
Copy code
create { "a" }
resolve to `create`(Unit)?
and for that, Kotlin sees that both
create
overloads are applicable, so it picks the most specific one
j
{"a"}
in
create({"a"})
does not resolve to
create<Unit>(()->S):S
even
Copy code
inline fun <reified S : Unit> create(code: () -> S): S {
    println("unit")
    return code() as S 
}
permits
{"a"}
e
https://pl.kotl.in/I2_Vj0UXU it does resolve to Unit
j
that's hello-world you pasted.
e
try again, sometimes playground doesn't pick up the state on the first load
j
$ kotlin Welcome to Kotlin version 1.5.31 (JRE 17+35-LTS-2724) Type :help for help, :quit for quit
>> {"a"}
res0: () -> kotlin.String = () -> kotlin.String
>>
still no cache update... more disturbing still, to me at least... is the REPL departure from the compiled lang.
e
maybe try in a different browser, it loads perfectly for me in an incognito tab and on other devices
you need to "return" S there, unless you change the return type to Unit
j
>> fun <S> create(code: () -> S): S {
...    println("string") ...    return code() ... }
>> 
>> inline fun <reified S : Unit> create(code: () -> S): S {
...    println("unit") ...    return code() as S ... }
>> 
>> fun main() {
...    create { "a" } // prints unit ...    val a = { "a" } ...    create(a) // prints string ... }
>> main()
unit string
>>
e
exactly.
create<Unit>{ "a" }
works, so it is picked, but
create<Unit>(a)
doesn't
j
val a={"a"} appears to cast a shadow.. edit: no that's not it.
e
line 4: Kotlin has already chosen
fun <S: Unit> create
, and then you write
create<String>
which doesn't fit. would be fine if you defined the unit-create without the type parameter
line 5: Kotlin doesn't do any type inference on a lambda in receiver position
j
unrelated
>> fun ((Int) -> Unit).f() = Unit
>> {x:Int->}.f()
>>
I'm really failling to find the discriminator criterion for "correct" here
Copy code
>>> create({"a"}as ()->Nothing?)       
string 
java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Void (java.lang.String and java.lang.Void are in module java.base of loader 'bootstrap') 
>>> create({"a"}as ()->Nothing) 
unit 
java.lang.ClassCastException: class java.lang.String cannot be cast to class kotlin.Unit (java.lang.String is in module java.base of loader 'bootstrap'; kotlin.Unit is in unnamed module of loader java.net.URLClassLoader @484203f3) 
>>> create({"a"}as ()->Int)       
string java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Number (java.lang.String and java.lang.Number are in module java.base of loader 'bootstrap')
Nothing?
fails to match
Unit
but
Nothing
matches, and
String
matches, but
Int
does not match, ok now i know the discriminator.
I must be missing the specification part that says ()->S is always Function1<Unit,S> except in the case of being unamed values.
e
inference influences the type of a lambda
Copy code
val a: () -> Unit = { "a" }
val b: () -> String = { "b" }
val x = a() // Unit
val y = b() // "b"
val c = (a as () -> String)() // ClassCastException
the
{ "a" }
and
{ "b" }
expressions look similar but create different types of functions (although erasure means the cast of the function succeeds, it's handling the result that fails)
although functions taking different numbers of arguments are observably different types even with erasure (until you go past certain limits)
Copy code
val a: (Any?) -> Unit = { "a" } // ok, "it" isn't used
    val b = { "b" } as (Any?) -> Unit // ClassCastException
j
disregarding the class cast exceptions, I disagree that we're seeing the first specialization as a programming contract to live by, proof is below; we're seeing something as reliable as hashtable ordering resolution for a Function<*,B> that Kotlin could and should verify at dispatch. parser grammar for receivers is one thing, and there's nothing wrong with certain parse conditions that could proceed but fail cautiously until you inform the type inference more. OP isn't posting about that. this isn't even return type erasure. this is just a misinterpretation of at a minimum due to the chosen code sample, https://en.wikipedia.org/wiki/Unit_type
>> create({"a"}as ()->Nothing?)
string
>> create({"a"}as ()->Nothing)
unit
e
the second one "works" because
() -> Nothing
is a
() -> Unit
, which is because
Nothing
is a
Unit
(and also every other type)
but
Nothing?
is not a
Unit
, so the first can't resolve to the more specific `create`(Unit)
j
if you hold fast to this then you are also saying that {"a"} is isomorphic to ()->Nothing and therefore matches ()->String and ()->Unit equally.
e
{ "a" }
is not a
() -> Nothing
now, Unit as a return type is handled rather specially, in that any value is simply ignored
j
but, in fact {"a"} is isomorphic to ()->Nothing in this example while {1} is not
e
hence
({ null } as () -> Unit)()
succeeds, even though
{ null }
is a
() -> Nothing?
here
but that's just an artifact of how the runtime works
just in case you didn't notice,
as
doesn't influence the inferred type of its left-hand side expression
j
>> create({"a"}as ()->Int)
string of loader 'bootstrap')
>> create({"a"}as ()->Unit)   
unit
>> create({1})    
unit
>> create({1}as ()->Int)
string res81: kotlin.Int = 1
say what again?
class cast exceptions are elided here
e
Copy code
fun <S: Unit> create(code: () -> S) = println(code()::class)
create { "a" } // class kotlin.Unit
create({ "a" } as () -> Unit) // class kotlin.String
j
this is an expensive language feature for the industry. maybe different IR will catch this or different backends will not behave as these jvm examples do but let me be the first to decline Kotlin as an embedded pacemaker development platform on this bug alone
e
I fully expect these to fail harder on Kotlin/Native, and nothing to fail on Kotlin/JS
by using
as
in this way, we are stepping outside of the defined semantics of Kotlin
j
we're not using
as
on {"a"}. we're debating the justification of a billion dollars of software industry rework without blinking.
e
I have no idea what you are talking about there.
j
this bug is substantial enough to derail the intentions and time of a programmer without necessarily being easily tracked down or identifiable. this kind of bug will hit the next tier of support, and the next tier, all the way up, and will likely persist until refactoring or some other major rework unknowingly hardens the cast.
e
I am failing to see what the bug is. type inference and overload resolution are working in consistent ways
j
that's a billion dollars when Kotlin/jvwhatevr is the official platform of the largest mobile device platform
>> "a"
res82: kotlin.String = a
>> {"a"}
res83: () -> kotlin.String = () -> kotlin.String
>> create({"a"})
unit
it sounds like you would bet a billion dollars on everyone agreeing with you
e
thinking through the ramifications, this is how the language has to work. if you changed that, then
Copy code
foo.also { set.add(it) }
would break without an additional
Unit
, because
add
returns a
Boolean
.
there are languages with different choices, for example Rust can make any lambda
()
-returning with just a trailing
;
, but that is not Kotlin
j
this usecase will absolutely show up in Label widget text code. guaranteed, someone will pose a lambda as a text provider interface and will pass all unit tests and automated browser/gui testing and breeze through QA with data driven lambdas which will eventually be passed an immediate mode call or a variable that can be inlined before checking in the code and shipping it.
foo.also { set.add(it) }
I'm completely, honestly fine with Unit<U:Any?-Any?(block:U?){block?.invoke()} in the language to bring about consistency or as a compiler inserted shim for weak platforms
e
that's a poor choice for other reasons:
fun <T> T.also(block: (T) -> Unit)
documents that the return value is not used, whereas your variant doesn't
j
the general notion is one of having a /dev/null vs. having a pretend one.
the whole argument where ()->Nothing is resolved because neither ()->Unit nor ()-String are verified as Function1<Unit,R> is some software orgnaizations peril times the number of software organizations using Kotline/j* lambdas who are going to learn this lesson by trusting Function1<T,R> as an implied contract