Does anyone know of an alternative for `?.let { } ...
# getting-started
g
Does anyone know of an alternative for
?.let { } ?: emptyList()
that lets you place the default value at the beginning of the
.let
statement? One of my few complaints with Kotlin is that it becomes hard to read in cases like this:
Copy code
val foo = maybe?.null?.let {
  // really long series of statements
} ?: someDefault()
I'd love something like:
Copy code
val foo = maybe?.null?.letWith(someDefault) {}
e
Copy code
value ?: orElse()
only evaluates
orElse()
when `value == null`; your
letWith
extension won't do that, unless it takes multiple lambdas, which is not very ergonomic
g
Ah hmm, there's really nothing in the stdlib for this then, is it? 😢
Copy code
inline fun <T> T?.withDefault(default: () -> T): T = this ?: default()
fun <T> T?.withDefault(default: T): T = this ?: default
Something like this ought to do it?
e
I don't see any benefit to that over the built-in syntax, and it doesn't help you with
letWith
well I suppose it binds differently so that an infix operator after it will have different precedence, but that's not related
g
I essentially just want to avoid this:
Where I can put the expected default value up front so it's clearer to the reader
e
that doesn't help you move it though
g
Ah I see what you're saying, it would still have to come last
🤦‍♂️
Okay I get what the problem is -- trying to create a default value in the middle of an expression chain means you need to create and store some kind of contextual stack until the end of the expression (I think)
Copy code
inline fun <T, R> T?.letWith(default: R, block: (T) -> R): R = this?.let(block) ?: default

val maybeNullStr: String? = null
val letWithExample = "foo".letWith("bar") { it.toUpperCase() } // "FOO"
val letWithExample2 = maybeNullStr.letWith("bar") { it.toUpperCase() } // "bar"
e
what I was saying earlier about multiple lambdas:
Copy code
inline fun <T : Any, R> T?.let(ifNull: () -> R, ifNotNull: (T) -> R): R = if (this == null) ifNull() else ifNotNull(this)

maybeNullStr.let(
    ifNull = { "foo" },
    ifNotNull = { it.uppercase() }
)
that would preserve laziness of the default, and does have some precedent in stdlib (
.associateBy()
has an overload taking multiple lambdas), but I don't think it's worth a standard addition
🙏 1
I feel that having a lazy default is important, and providing both a lambda and immediate overload can lead to difficulties in confusing overload selection if your receiver is itself a function type
g
Yeah -- those are good points, thanks. It's probably not worth adding an extension function for I suppose. The only one in my org's repo so far is
Any.prettyPrint()
, extension functions are a lot of magic.
e
I also have a non-serious suggestion :)
Copy code
maybeNullStr.let {
    it ?: return@let "foo"
    it.uppercase()
}
🤯 1
g
🤔 blob thinking fast blob think smart
I actually like that, lol
k
For your example of
?.let {} ?: emptyList()
you can use
.orEmpty()
, for other values that don't have it you'd have to define your own extension, we have one in our project called
.ifNull { }
, it's useful when you're chaining operations:
Copy code
inline fun <T> T?.ifNull(block: () -> T): T = this ?: block()
e
that's exactly
withDefault
above by a different name - doesn't achieve the reordering OP was looking for
k
I'm not sure to what you refer with the "reordering" 🤔
From what I understood the reason is making chained operations clearer to read:
Copy code
val foo = maybe
    ?.null
    ?.let { 
        // ...
    }
    .ifNull {
        someDefault()
    }
As
let
is usually used for small pieces of code, if OP is writing a considerable amount of statements in there they may want to consider moving that logic to a function.
k
e
they didn't think it all the way through
anyhow, yes my style would be to split up the expression chain if it got long enough to result in confusion, but that is more of a sidestep than an answer to OP's question
g
As
let
is usually used for small pieces of code, if OP is writing a considerable amount of statements in there they may want to consider moving that logic to a function.
This is a fair point to be honest, the argument could be made that the ~50 lines in the
let
block ought to be extracted
1
j
I came here to say just that, but I saw the discussion led there eventually lol
Like literally if you can "abstract away" a block of code behind a white square in a screenshot and say "this thing here", it means it should really be a function 🙂
a
How about regular if checks?
j
The contents of the
if
and
else
clause should IMO be very short too for clarity, so yes this would imply extracting a function too for each side depending on the code
a
Yeah, I would write something like this:
Copy code
fun f(x: X?): List<Y> {
    if (x == null)
         return emptyList()
     
    // use non-null x here
}
Or move the if check to before calling this function
j
Yep, one or the other looks clear to me, although in this specific case, taking a null input seems wrong to me. Do meaningful business stuff in functions with nice names, and then deal with little edge cases outside of it. If you already extracted this function with non-nullable param, it will be short and clear to deal with the null case:
Copy code
// some code
val list = x?.let { makeAListOutOf(it) ?: emptyList()

fun makeAListOutOf(x: X): List<Y> {
    // use non-null x here
}
But depending on the meaning of the function, you might want to make it an extensions and it's even easier to deal with nulls then:
Copy code
// some code
val list = x?.transformSomehow() ?: emptyList()

fun X.transformSomehow(): List<Y> {
    // use non-null x here
}
Usually for all functional operators like
map
/
filter
and the likes, I also find it clearer if the body of such functions are single expressions (usually a function call, often an extension function call that makes it read nicely like english, for instance
things.map { it.toSomeOtherThing() }
)