Hi there! Does the following make sense? I'm total...
# announcements
g
Hi there! Does the following make sense? I'm totally new to Kotlin and just experimenting
Copy code
fun <T> T?.onNull(effects: () -> Unit): T? {
    if (this == null) effects()
    return this
}
What I feel strange is that by defining the above extension, I'm allowed to invoke it on
non-nullable
types, which results a little counter-intuitive. I mean, given the above I can do:
Copy code
val str: String = "helo"  // non-nullable type
str.onNull { print("Will never print as I'm not actually null") }  // <-- Why the compiler allows this?
Is it possible to restrict it just to nullables as in
T?
Thx 🙂
m
You cannot restrict it to just nullable types. I have this:
Copy code
@OptIn(ExperimentalContracts::class)
inline fun <T : Any> T?.ifNull(defaultValue: () -> T): T {
	contract {
		callsInPlace(defaultValue, InvocationKind.AT_MOST_ONCE)
	}

	return this ?: defaultValue()
}
n
the elvis operator basically does this though.
val foo = thisMightBeNull ?: effects()
m
You could add a non-null overload and deprecated it:
Copy code
@JvmName("ifNullOnNonNull")
@Deprecated(message = "Use on nullable types only.", replaceWith = ReplaceWith("this"), level = DeprecationLevel.ERROR)
inline fun <T : Any> T.ifNull(defaultValue: () -> T): T =
	this
@nanodeath the elvis operator is not chain-friendly
z
This is a giant hack, but I think you can do something like this:
Copy code
fun <T> T?.onNull(effects: () -> Unit): T? {
  if (this == null) effects()
  return this
}

@Suppress("UNUSED_PARAMETER", "unused")
@Deprecated("Only supported for nullable types.", level = ERROR)
@JvmName("onNull-non-nullable")
fun <T : Any> T.onNull(effects: () -> Unit): T {
  throw UnsupportedOperationException("Only supported for nullable types.")
}
Because the bounded-T overload is more specific, it’s the overload that will be selected for non-nullable types. The ERROR-level deprecation causes the compiler to then complain when that happens.
g
Wow! 👏 👏 Thanks for all the feedback! Overloading the more concete (nullable type) works! By doing that, the compiler forbids to invoke it 🙂 @Zach Klippenstein (he/him) [MOD] And yes, the elvis operator is not chain friendly as it does not return
this
, I wanted to use it in operation pipelines for example to log on null.
Is there any situation in which
UnsupportedOperationException
would actually be thrown? or the compiler would *always/*in all cases make it fail at compile-time?
c
Not really related, but if you ever need the type of
null
itself, it's
Nothing?
m
Null is afaik the only value that conforms to
Nothing?
but it's not the same thing as "the type of null"
m
🤔
🤔 1
z
Is there any situation in which that 
UnsupportedOperationException
 would actually be thrown?
I think consumers could
@Suppress("DEPRECATION")
to let them call that function.
g
And would replace
T?
with
Nothing
for the overloaded one:
Copy code
fun <T : Any> T.onNull(effects: () -> Unit): Nothing {
make any difference or improvement? I've seen the
Nothing
doc:
Nothing has no instances. You can use Nothing to represent "a value that never exists": for example, if a function has the return type of Nothing, it means that it never returns (always throws an exception)
e
About elvis operator not being chain friendly, you can always use it inside scope functions. For example:
Copy code
doSomethingThatMayReturnNull()
  .also { it ?: println("Null") }
  .doSomethingElse()
Although the
ifNull
approach is more idiomatic
g
Thanks for the point @edrd 🙂 Yes,
onNull
/
ifNull
easier to read indeed. Anyone has thoughts about using
Nothing
as the return type of the overload that throws instead of
T?
, any difference? - Both seem to work
👍 1
c
I don't think it's a good idea to use
Nothing
here: it means “this function doesn't return”, not “you shouldn't call this function”. Since you have the error depreciation and the
throw
inside the function, it won't make a difference. But I don't think you should have this function at all.
g
Hi @CLOVIS, thanks! Actually the function never return, just throws which is different from returning. But what do you mean by 'you should have this function at all'? :)
c
I don't really like that you have to define a useless function that shouldn't be used. There must be a better pattern somewhere. Maybe even just using
.also { it ?: effects() }
would be better.
g
.also { it ?: effects() }
.onNull { effects() }
if I read both of them, I personally find the first has more cognitive complexity and is less clear (also more verbosity). The
onNull
encapsulates that behaviour, looks a good abstraction to me. But I'm also interested in hearing more oppinions 🙃
e
Back to the original question, i.e. "Does the following make sense?": I'm not a type systems expert, but intuitively if the extension receiver has a nullable type, it should only be callable from that type. @elizarov, would appreciate your thoughts.
m
I also find constructs like
.let { it ?: foo }
difficult to parse esp. when the expressions become longer or more complex. I have 104 matches of
.ifNull
in my project. It’s very simple and useful :) Btw, it should be
ifNull
if it returns something, analogous to
ifEmpty
of
CharSequence
and
Collection
.
on…
is typically for side-effects and only returns
this
unchanged, analogous to
onEach
of
Iterable
. See my example: https://kotlinlang.slack.com/archives/C0922A726/p1616006471054600?thread_ts=1616006300.054500&amp;cid=C0922A726
Calling
.ifNull
on a non-null type is like calling
?.anything
on a non-null receiver. The IDE warns about that. Unfortunately we have no way of telling the compiler or IDE to issue a warning or error other than using the overload trick.
g
I started talking about
onNull()
as my intention was a function to just perform just side-effects (see the Unit in the signature), the same way
Option.onEmpty()
would do. But as it applies over a nullable type instead of over a container, it should be
onNull()
. Then I also thought about an
Copy code
fun <T> T?.orElse(ifNull: (T?) -> T?): T?
to return another thing like a fallback.
m
If your
onNull
returns
this
then it makes sense 🙂
e
Note that the default generic upper bound is
Any?
, so you could just write
Copy code
fun <T> T.orElse(ifNull: (T) -> T): T
But I think it would make more sense to have
orElse
return a non-nullable, otherwise you could just use `let`:
Copy code
fun <T : Any> T?.orElse(ifNull: (T?) -> T): T
Or if you want to allow another return type:
Copy code
fun <T, R> T.orElse(ifNull: (T) -> R): R
c
I agree with you @Gerard Bosch, the callsite looks much more readable. The declaration site doesn't do, but maybe a big fat comment “this is a hack that is used to ...” is enough to handle that. I'd probably call it
ifNull
and not
onNull
though.
m
The declaration site is quite readable. Just the overload workaround should be documented as that’s not self-explaining.
👍 1
Fun fact: the way
ifEmpty
is implemented by stdlib isn’t legal Kotlin code. You cannot just copy & paste it :)
c
Really? Where is it?
m
Just Cmd-Click the function invocation.
Copy code
public inline fun <C, R> C.ifEmpty(defaultValue: () -> R): R where C : Collection<*>, C : R =
    if (isEmpty()) defaultValue() else this
They do that to allow the result to be nullable or non-nullable depending on your
defaultValue
lambda. Pretty neat.
c
Oh yeah, the double where. For the same reason, they can't add
filterIsInstance<A, B>()
m
What would that do? 🤔
c
Check if an object implements two interfaces. It's pretty much the same as doing
.filterIsInstance<A>().filterIsInstance<B>()
, except it would be safe (if you write that, you get
C<B>
, but you'd want
C<A & B>
(using Java notation). The weird part is that if you have two specific interfaces, you can define that (
where C : InterfaceA, C : InterfaceB
) but that doesn't work with generics, even when
inline
.
m
That should be part of the request to add intersection and union types 🙂
Copy code
.filterIsInstance<A & B>()

.filterIsInstance<A | B>()
g
The benefit that I see of
.orElse()
over
.let
is at the call site, as if I'm not wrong, the
let
requires to call like
?.let { }
to achieve that behaviour. Forgotting the
?
is very easy and also confusing. WDYT?
Copy code
fun <T> T?.orElse(ifNull: () -> T?): T? {
    return this ?: ifNull()
}
m
I wouldn’t use
orElse
. It typically refers to absence of value, not
null
. What do you mean by forgetting
?
?
g
Maybe I'm wrong, but is
Copy code
something()
  ?.let { somethingElse() }
the same than?
Copy code
something()
  .let { somethingElse() }
If I'm not wrong the above has different behaviours, hasn't it? And
Copy code
something()
  .orElse { somethingElse() }
would cause no confusion. I'm thinking all of this but I'm a Kotlin newbie, so I may be wrong 🙂
Sorry, I think I'm confusing with
.let { it ?: foo }
, don't take the above into account.
m
If you have
.let { it ?: foo }
and
?.let { it ?: foo }
the IDE would warn about the latter making no sense.
g
So the discussion would be the same than `.onNull()`:
Copy code
.let { it ?: somethingElse(...) }
vs
Copy code
.orElse { somethingElse(...) }
for readability. Does it make sense now?
y
No one has mentioned this yet, but this is possible with some internal annotation magic-y things:
Copy code
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
import kotlin.internal.*
inline fun <T: Any> @Exact T?.onNull(effects: () -> Unit): T? {
    if (this == null) effects()
    return this
}

fun main() {
    val str: String = "hello"  // non-nullable type
	str.onNull { println("The compiler prevents this call from compiling") }
	val nullableStr: String? = str
    nullableStr.onNull { println("but this is allowed")}
	val nullableStr2: String? = null
    nullableStr2.onNull { println("only this is null though")}
    println("hello world")
}
😮 2
👏 1
g
Thank you @Youssef Shoaib [MOD]!! So the answer to the original question is: "YES, there is a way for the compiler to prevent calling this
.onNull
over non-nullable type." 👏 👏 👏 But maybe this is a bigger hack than overloading the function, and more fragile I guess? If the internal annotations change in future versions. WDYT? Also the error message in the IDE is more cryptic compared to the deprecated message.
m
Suppressing error message is planned to be removed from Kotlin
y
Well then you can use this trick then to shadow the internal annotation
😬 1
@Marc Knaup that's actually where I got the INVISIBLE_MEMBER trick from. Also, in response to this: https://kotlinlang.slack.com/archives/C7L3JB43G/p1615835134021200?thread_ts=1615833497.020200&amp;cid=C7L3JB43G
m
Yeah but it’s still not decided if and which annotations will be made public.
It would be great to have access to everything that the stdlib uses :)
1
g
So with all this inputs it looks just safer to use the overload with
@Deprecated
m
That’s the only approach that doesn’t rely on internal behavior.
👍 1
g
I liked the creativity of all these hacks and shadows BTW 🙂
m
My libraries and projects are full of hacks 🙈 One day they become features I hope.
😃 2
g
Sorry for repeating @Marc Knaup So the discussion would be the same than `.onNull()`:
Copy code
.let { it ?: somethingElse(...) }
vs
Copy code
.orElse { somethingElse(...) }
for readability. Does it make sense now?
What do you think?
m
Again,
orElse
is a bad fit 🙂 https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/get-or-else.html But yeah, I’d avoid mixing
.let
with
?:
.
g
Do you mean
getOrElse
suits better?
m
No,
OrElse
is for absence of value, not for
null
value.
If you function returns a replacement for
null
then I’d use
ifNull
similar to
ifEmpty
which already exists.
g
Yep, but I'm trying to mimic what an Option is but sticking to nullable-types
m
There is no
Option
in Kotlin
g
I know, that's why I try to mimic it, hehe
Using the nullable-type as the Option itself
m
Then you should implement an
Option.orElse
😉
g
It's just some ideas and experimentation while I'm learning
m
Java’s
Optional.orElse
comes closest. There the name would fit Kotlin’s naming scheme because it would return something if there’s an absence of value.
null
on the other hand is not absence.
g
All around the codebase I'm working right now the
null
is used as a semantic value of 'no result'
m
Yeah that’s fine. As is using
ifNull
then 🙂 Super clear just from reading it.
g
Then I will have
onNull(effects)
and
ifNull(fallback)
? not confusing?
m
No it’s not. Because in Kotlin with
if…
the lambda always typically returns a default value and and
on…
always typically returns
this
and is only a side-effect. Consistency matters here.
g
Thank you! 👍
m
Copy code
listOf(1,2,3)
    .onEach { println(it) }
    .ifEmpty { null }
👍 1
🎉 1