Gerard Bosch
03/17/2021, 6:38 PMfun <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:
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 🙂Marc Knaup
03/17/2021, 6:41 PM@OptIn(ExperimentalContracts::class)
inline fun <T : Any> T?.ifNull(defaultValue: () -> T): T {
contract {
callsInPlace(defaultValue, InvocationKind.AT_MOST_ONCE)
}
return this ?: defaultValue()
}
nanodeath
03/17/2021, 6:44 PMval foo = thisMightBeNull ?: effects()
Marc Knaup
03/17/2021, 6:44 PM@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
Marc Knaup
03/17/2021, 6:45 PMZach Klippenstein (he/him) [MOD]
03/17/2021, 6:45 PMfun <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.Gerard Bosch
03/17/2021, 8:08 PMthis
, I wanted to use it in operation pipelines for example to log on null.Gerard Bosch
03/17/2021, 8:12 PMUnsupportedOperationException
would actually be thrown? or the compiler would *always/*in all cases make it fail at compile-time?CLOVIS
03/17/2021, 8:35 PMnull
itself, it's Nothing?
Michal Klimczak
03/17/2021, 8:39 PMNothing?
but it's not the same thing as "the type of null"Marc Knaup
03/17/2021, 8:41 PMZach Klippenstein (he/him) [MOD]
03/17/2021, 9:22 PMIs there any situation in which thatI think consumers couldwould actually be thrown?UnsupportedOperationException
@Suppress("DEPRECATION")
to let them call that function.Gerard Bosch
03/17/2021, 11:03 PMT?
with Nothing
for the overloaded one:
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)
edrd
03/18/2021, 12:30 AMdoSomethingThatMayReturnNull()
.also { it ?: println("Null") }
.doSomethingElse()
edrd
03/18/2021, 12:31 AMifNull
approach is more idiomaticGerard Bosch
03/18/2021, 10:40 AMonNull
/ 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 workCLOVIS
03/18/2021, 11:32 AMNothing
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.Gerard Bosch
03/18/2021, 1:26 PMCLOVIS
03/18/2021, 2:00 PM.also { it ?: effects() }
would be better.Gerard Bosch
03/18/2021, 2:40 PM.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 🙃edrd
03/18/2021, 3:18 PMMarc Knaup
03/18/2021, 3:18 PM.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&cid=C0922A726Marc Knaup
03/18/2021, 3:20 PM.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.Gerard Bosch
03/18/2021, 3:30 PMonNull()
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
fun <T> T?.orElse(ifNull: (T?) -> T?): T?
to return another thing like a fallback.Marc Knaup
03/18/2021, 3:33 PMonNull
returns this
then it makes sense 🙂edrd
03/18/2021, 3:34 PMAny?
, so you could just write
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`:
fun <T : Any> T?.orElse(ifNull: (T?) -> T): T
Or if you want to allow another return type:
fun <T, R> T.orElse(ifNull: (T) -> R): R
CLOVIS
03/18/2021, 3:35 PMifNull
and not onNull
though.Marc Knaup
03/18/2021, 3:37 PMMarc Knaup
03/18/2021, 3:38 PMifEmpty
is implemented by stdlib isn’t legal Kotlin code. You cannot just copy & paste it :)CLOVIS
03/18/2021, 3:39 PMMarc Knaup
03/18/2021, 3:40 PMpublic inline fun <C, R> C.ifEmpty(defaultValue: () -> R): R where C : Collection<*>, C : R =
if (isEmpty()) defaultValue() else this
Marc Knaup
03/18/2021, 3:40 PMdefaultValue
lambda. Pretty neat.CLOVIS
03/18/2021, 3:52 PMfilterIsInstance<A, B>()
Marc Knaup
03/18/2021, 3:52 PMCLOVIS
03/18/2021, 3:56 PM.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
.Marc Knaup
03/18/2021, 3:57 PMMarc Knaup
03/18/2021, 3:57 PM.filterIsInstance<A & B>()
.filterIsInstance<A | B>()
Gerard Bosch
03/18/2021, 4:27 PM.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?
fun <T> T?.orElse(ifNull: () -> T?): T? {
return this ?: ifNull()
}
Marc Knaup
03/18/2021, 4:34 PMorElse
. It typically refers to absence of value, not null
.
What do you mean by forgetting ?
?Gerard Bosch
03/18/2021, 4:39 PMsomething()
?.let { somethingElse() }
the same than?
something()
.let { somethingElse() }
If I'm not wrong the above has different behaviours, hasn't it?
And
something()
.orElse { somethingElse() }
would cause no confusion. I'm thinking all of this but I'm a Kotlin newbie, so I may be wrong 🙂Gerard Bosch
03/18/2021, 4:43 PM.let { it ?: foo }
, don't take the above into account.Marc Knaup
03/18/2021, 4:50 PM.let { it ?: foo }
and ?.let { it ?: foo }
the IDE would warn about the latter making no sense.Gerard Bosch
03/18/2021, 4:51 PM.let { it ?: somethingElse(...) }
vs
.orElse { somethingElse(...) }
for readability. Does it make sense now?Youssef Shoaib [MOD]
03/18/2021, 5:39 PM@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")
}
Gerard Bosch
03/18/2021, 6:06 PM.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.Marc Knaup
03/18/2021, 6:07 PMMarc Knaup
03/18/2021, 6:09 PMYoussef Shoaib [MOD]
03/18/2021, 6:11 PMYoussef Shoaib [MOD]
03/18/2021, 6:13 PMYoussef Shoaib [MOD]
03/18/2021, 6:13 PMMarc Knaup
03/18/2021, 6:14 PMMarc Knaup
03/18/2021, 6:14 PMGerard Bosch
03/18/2021, 6:18 PM@Deprecated
Marc Knaup
03/18/2021, 6:18 PMGerard Bosch
03/18/2021, 6:22 PMMarc Knaup
03/18/2021, 6:23 PMGerard Bosch
03/18/2021, 6:26 PM.let { it ?: somethingElse(...) }
vs
.orElse { somethingElse(...) }
for readability. Does it make sense now?Gerard Bosch
03/18/2021, 6:27 PMMarc Knaup
03/18/2021, 6:28 PMorElse
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 ?:
.Gerard Bosch
03/18/2021, 6:29 PMgetOrElse
suits better?Marc Knaup
03/18/2021, 6:29 PMOrElse
is for absence of value, not for null
value.Marc Knaup
03/18/2021, 6:30 PMnull
then I’d use ifNull
similar to ifEmpty
which already exists.Gerard Bosch
03/18/2021, 6:30 PMMarc Knaup
03/18/2021, 6:30 PMOption
in KotlinGerard Bosch
03/18/2021, 6:30 PMGerard Bosch
03/18/2021, 6:30 PMMarc Knaup
03/18/2021, 6:30 PMOption.orElse
😉Gerard Bosch
03/18/2021, 6:31 PMMarc Knaup
03/18/2021, 6:31 PMOptional.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.Marc Knaup
03/18/2021, 6:32 PMGerard Bosch
03/18/2021, 6:33 PMnull
is used as a semantic value of 'no result'Marc Knaup
03/18/2021, 6:33 PMifNull
then 🙂
Super clear just from reading it.Gerard Bosch
03/18/2021, 6:34 PMonNull(effects)
and ifNull(fallback)
? not confusing?Marc Knaup
03/18/2021, 6:35 PMif…
the lambda on…
this
and is only a side-effect.
Consistency matters here.Gerard Bosch
03/18/2021, 6:36 PMMarc Knaup
03/18/2021, 6:36 PMlistOf(1,2,3)
.onEach { println(it) }
.ifEmpty { null }