In this example `foo` is not being smart casted to...
# announcements
s
In this example
foo
is not being smart casted to non-null even tho it cannot be null in that else block. Shouldn’t the compiler be smart enough to infer this?
Copy code
class Foo(val str: String? = "")
val foo: Foo? = null
if (foo?.str.isNullOrBlank()) {
   //...
} else {
   println(foo.str) // doesn't compile. foo is not smart-casted to a non-null type
}
Btw if the condition was
foo?.str == null
, this would compile. so it’s not impossible to “backtrack” from
str
to
foo
in this case
j
The contract is for
foo?.str
, not
foo
. This works:
Copy code
class Foo(val str: String? = "")
val foo: Foo? = null
val str = foo?.str
if (str.isNullOrBlank()) {
  //...
} else {
  println(str)
}
Does the difference make sense?
s
Yeah, I understand why it works like that, but I’m saying that just like
foo?.str != null
leads to smartcasting of
foo
and not just
str
, so should the other cases
j
foo?.str?.takeIf { it.isNotBlank() }?.let { println(it) }
s
Well the actual code would be something like
Copy code
val result = if (foo?.str.isNullOrBlank()) {
  ""
} else {
  "some format of $it"
}
And sure, I could be all fancy and do it like this
Copy code
val result = foo?.str
   ?.takeIf { it.isNotBlank() }
   ?.let { "some format $it" }
   ?: ""
But I think that’s less readable than
if it's valid, format it. else use a blank
But the question is not how to get around this limitation. I’m just pointing out that this shouldn’t be a limitation.
j
There are a lot of improvements that can be made.
n
the tricky thing I guess is that no matter how much you extend it, there will always be the case just slightly more complex than that the compiler can't figure out. And if you start supporting this kind of non-immediate inference, then you could argue it could lead to somewhat "fragile" code
people write code depending on it, and then a small change extends it just past that boundary the compiler can figure out, and now you have to rewrite the code that depended on the smart cast.
s
By that logic
if (foo?.str != null)
shouldn’t infer
foo
to be non-null
n
not really, depends what you consider direct, I think
s
Also it’s a pretty thin line of reasoning, since it doesn’t break existing code, just if you introduce a change, which is already true. Consider this code:
Copy code
class Bar(val b: Bar? = null, val s: String? = null)
val bar: Bar? = null
if (bar?.b?.b?.b?.s != null) {
   println(bar.b.b.b.s.length)
}
it compiles fine, but if tomorrow you decide “oh wait, it’s not just null, I want to make sure
s
isn’t empty either”, you’ll lose all the smartcasting. So should
bar.b.b.b
not have worked in the first place?
n
You won't lose the smartcasing if you && the conditions. How were you planning to make the change?
s
!bar?.b?.b?.b?.s.isNullOrEmpty()
for example. Sure it can be avoided by a different combination of conditions, but I don’t think && would be very idiomatic in this case
n
sure, this is pretty equivalent to your original example then, not sure why you made it recursive.
I'm only saying that the current system, afaics, is pretty straightforward to reason about, it doesn't seem to propagate the information at all out of the immediate context. there is some value to that.
In
foo?.str != null
, then effective
?.
is a built in function with
foo
and
str
(in a sense) as arguments; when the result is not null it applies the contract to the immediate arguments
s
So why should the
?.
function have a better contract than
.isNullOrEmpty
?
or rather, why should
!= null
have a better contract than
isNullOrEmpty
n
it doesn't
isNullOrempty has one argument
if you are in a branch where isNullOrEmpty is false, then its one argument will be smart-cast non-nullable
why do you think ?. has a better contract?
If there's a function getting special treatment here it's actually == / !=, because even though it's technically the top level function, the compiler will go down one level
s
yup, that’s what I meant. It’ll go down way more than one level
n
Way more?
alright, I see what you mean.
what's tricky with contracts is that they can be arbitrarily complex, so it's more of a commitment to try to maximally propagate contract knowledge, compared to a handful of language built ins
s
Yeah, i agree it’s more work to achieve this functionality, but ultimately I would expect it from stblin at least
j
Kotlin is too good to us, we get spoiled 🙂
s
haha ok I can definitely agree on that
n
i don't think this is something that could be fixed in the stdlib, it's a core language issue. I'm too lazy but it would be interesting to construct an example just with two functions w/ contracts
and prove that information doesn't propagate back up past the last-evaluated contract
I think coming from C++, it's worth being cognizant that you add more and more compile time power, and without realizing it, you discover that you have a Turing complete mini-language based on smart-casting 😂
😂 1