because you can, at some point, chain a function c...
# language-proposals
k
because you can, at some point, chain a function call without the
?
that will be invoked even if the receiver is
null
. For example,
System.getProperty("os.name")?.toLowerCase().isNullOrEmpty()
, and a distinction must be made if you want that function to execute no matter what, or only when the receiver is non-
null
b
But that’s my point;
.toLowerCase()
will never ever return
null
. It’s promised that in its signature with
: String
instead of
: String?
. We know this at compile-time. In your example,
.isNullOrEmpty()
can be called in my
isMac2
and
isMac3
examples, too, and the compiler also warns that this is silly, with a warning saying “Call on not-null type may be reduced”
i
The expression
osName?.toLowerCase()
will have
null
value if
osName
is
null
, therefore the type of this expression is
String?
b
I’m trying to say that my proposed syntax refinement
osName?.toLowerCase().startsWith()
would also have type
String?
, but you just wouldn’t be required to type as many `?`s since the compiler knows that if you got far enough to run the first function, you are no longer dealing with `null`s and can safely call the second without using the
?.
operator.
r
And want if you also have a
fun String?.startsWith()
that has a little different functionality?
k
^ Exactly, calling a function on a nullable type is just as valid as calling it on a non-null one. You absolutely need the
?.
between each function call because it makes it explicit that the function is going to be called only if the preceding type is non-null or if it will be called unconditionally
5
b
I agree; it’s important to be able to call funcitons on nullable types… I’m not saying anything to the contrary. If you think I am, please re-read my proposal. I just want to change this:
Copy code
nullable?.neverNull()?.foo
to this:
Copy code
nullable?.neverNull().foo
3
i
So if
foo
is an extension for nullable type (like
let
or
isNullOrEmpty
) shall it be called in the last example or not?
b
@ilya.gorbunov in my last example?
nullable?.neverNull().foo
? since
neverNull()
is only called if
nullable
is not
null
, then
foo
will also only be called if
nullable
is not null. This will stay consistent with the current behavior. I don’t see the confusion point here.
i
The current behavior is that
foo
is called in any case.
p
Yes,
foo
can be extension property declared on nullable type like this:
val String?.foo get() = this ?: "foo"
. So if
nullable
is
null
then whole expression will be evaluated to
"foo"
.
e
I totally agree with Ben and I also raised the same question in the past.. Imho, in the moment
nullable
is
null
, the code flow should jump at the
?:
If
foo
gets called in any case then it's wrong from a logic point of view
r
@elect That would break any code that has an extension (funciton or property) on a nullable type.
2
e
nullable?.neverNull().foo
, I'd expect here foo to be callable on not-nullable type
r
If
neverNull()
returns
Thing
, then
nullable?.neverNull()
returns
Thing?
. I could define a
val Thing?.foo
that will (and should) always be called. For example:
Copy code
class Thing(val foo: String)
class Other(val name: String) {
    fun neverNull() = Thing(name)
}
val Thing?.foo get() = this?.foo ?: "[bad thing]"
val nullable: Other? = null
println(nullable?.neverNull()?.foo)  // prints `null`
println(nullable?.neverNull().foo)  // prints `[bad thing]`
This is a bit of a contrived example, but I could put very useful functionality in there that I would expect to run precisely when it IS
null
.
e
println(nullable?.neverNull().foo)
, if
nullable
is
null
, I'd expect it to println directly
null
r
And I may have extra calls that come after the
.foo
call, so
?:
is not always the answer (as it would require many nested
(thing.foo ?: default).nextCall()
Why would you expect that if I defined
Thing?.foo
?
e
I see it this way, if you have a chain of operations
a()?.b()?.c()
, the very first step returning a
null
should automatically make the code flow jump at the
?:
if any, otherwise simply returns
null
r
Why? That would make extensions to nullable types pointless. For example, the super useful kotlin standard library function
Any?.toString()
e
sorry, expressed it wrongly
let's keep again the example of ben,
println(nullable?.neverNull().foo)
, if
nullable
is
null
and there is no
?:
, I expect to read
null
in console.
otherwise, as he also stated, the second step,
neverNull()
cannot produce a nullable output, that's why there should be no additional
?
and of course
neverNull()
will be executed only if
nullable != null
...
I mean, it looks really logic and natural for me
r
.neverNull()
cannot produce a null, but
?.neverNull()
can.
e
I know, but again, in case of
null
, I'd expect the code flow to skip any further call, that is
.neverNull()
, and jump directly to
?:
if any, or returns directly
null
r
Why would you expect it to skip if Kotlin specifically allows further calls to handle nulls?
e
because I interpret
?.
as "go on only if not null"
moreover, if you have an extension function handling nullable types, there is not point using
?
r
But that's not what
?.
means.
It's a save call operator, not a short cicuit operator (though I can see where you might be confused. Other languages like Rust do indeed use it as a short circuit operator).
e
https://kotlinlang.org/docs/reference/null-safety.html#safe-calls
b?.length
returns
b.length
if
b
is not
null
, and
null
otherwise.
r
Correct. it says nothing about short circuiting. If I have a member function that returns null would you expect execution to break and short circuit? I wouldn't. I would expect to be able to handle the null.
e
can you provide a short sample?
r
What kind of example. I provided one higher up that you responded to.
e
a reason to not short circuit
r
(By the way, we may want to move this to a DM. I'm not sure if this discussion is still usefull to this channel.)
I think my
Thing?.foo
was a pretty good example of that, also the standard library
Any?.toString()
i
Another example is `let`/`run`/`also` functions, they can be called on nullable receiver too
3
e
I guess you meant
class Wrapper
instead
class Other
r
Ah, I was thinking faster than typing. I fixed it.
b
Wow that's a lot of comments since I went to sleep! I wish I were here earlier to say that, in the case that
foo
was declared as an extension funciton on an optional type, one could just use the ever-disambiguating parentheses (that should always be used when writing confusing/niche syntax) like this:
(nullable?.neverNull()).foo
. Simple, self-explanatory, and easy for maintainers/newcomers to understand what's going on! Additional examples:
(nullable?.neverNull()).toString()
would return
"null"
(current behavior), whereas
nullable?.neverNull().toString()
would return
null
. (just like today's
nullable?.neverNull()?.toString()
)
(nullable?.neverNull()).let { ... }
would call
let
where
it
is
null
(current behavior), whereas
nullable?.neverNull().let { ... }
would never have its
let
block called at all. (just like today's
nullable?.neverNull()?.let { ... }
)
@Ruckus Rust, Swift, C#, and I'm sure others have that same behavior I want out of the
?.
operator. It's bizarre and confusing that Kotlin doesn't.
r
@benleggiero Personally I find your proposal way more confusing. Since chained function calls are executed in order, the idea that
(nullable?.neverNull()).foo
and
nullable?.neverNull().foo
return different values is mind boggling. I don't know about Swift and C#, but in Rust the short circuit is quite a bit different. It does an immediate return from the current function with an error result, not just a skip to the end of the line, so it's not so much a short circuit as an early return.
i
Kotlin just follows the logic easier to reason about:
.foo()
is called always and
?.foo()
is called only if receiver is not null. No matter where it is in a chain of calls.
3
😕 1
k
Even if anyone wanted this, I'm not sure how you would change this behavior without breaking everyone's existing code
b
@kevinmost I’m under the impression that Kotlin is versioned using Semantic Versioning. If that’s the case, this and other breaking language changes could simply be distributed under a MAJOR version increment, rather than a MINOR as we’ve so far seen.
👍 1
k
They're not "just" going to make Kotlin 2.0.0 though. The language has gone through years of improvements without needing a major increment. They wouldn't do it lightly, and they certainly wouldn't do it for a cosmetic feature that will fix nothing and introduce a whole bunch of issues with existing semantics and make the language less expressive and less powerful, all to save one keystroke