y
05/13/2025, 5:09 AMif let
in Rust and Swift), what's your favorite way of expressing this in Kotlin?
1. with scope functions:
parseData(data)?.also { /* ... */ }
2. with flow-sensitive typing:
val parsed = parseData(data)
if (parsed != null) { /* ... */ }
3. some other way?
1
feels more idiomatic but not quite first-class.Edgar Avuzi
05/13/2025, 5:40 AMSeop
05/13/2025, 5:57 AMalso
when performing additional actions, such as logging. So I prefer option 2.y
05/13/2025, 6:02 AMalso
semantically feels like "this is an additional side-effect that we can easily remove in the future if we don't want this" (so exactly like logging), not "this is the control flow in this code snippet".y
05/13/2025, 6:07 AMlet
for side-effects, which I see often)Vampire
05/13/2025, 6:56 AMapply
if you don't like also
and let
? :-DVampire
05/13/2025, 6:56 AMrun
CLOVIS
05/13/2025, 7:14 AMapply
is for "I want to apply additional configuration to this object", I wouldn't use it for flow-controlCLOVIS
05/13/2025, 7:16 AMif
here, or maybe ?:
depending on the situation.y
05/13/2025, 7:16 AMy
05/13/2025, 7:19 AMtakeUnless
, isNotEmpty
etc.) and I do appreciate thatCLOVIS
05/13/2025, 7:28 AMlet
is for pure functions that convert data. let
shouldn't be more than a single function call (.let { it + 2 }
, .let { it.toString() }
, .let(::toDto)
, .let(Instant::parse)
)
• ?.let
should be avoided (otherwise people create long chains where it's impossible to know what can be null or not).
• also
is for side effects that are unrelated to the primary goal of the function. For example, logging, notifying an other system… All also
calls in a function can be removed and the function should serve the same purpose.
• ?.also
is ok.
• There should never be an also
chain.
• apply
is to apply additional configuration to an object you just created or got from elsewhere (ArrayList<Int>().apply { add(5) }
, but there is buildList
for that purpose now).
• run
without receiver is for declaring lexical scopes (to delete local variables before the end of a long function) or to have more complex logic in a field's initialization (val a = run { … }
)
• run
with receiver I never really use
• takeIf
/`takeUnless` exist to ignore a subset of the possible values of a type before passing a value to another function. "foo".indexOf('a').takeIf { it >= 0 }
because the -1
case is illegal and we don't want it.y
05/13/2025, 8:20 AMval a = run { ... }
or val b = foo()?.bar()?.baz() ?: run { ... }
are less error-pronetrevjones
05/13/2025, 3:26 PM?: run { ... }
in a PR is almost always going to be a merge blocker.
Using nullability chaining + scope functions can lead to some really hard to read and/or refactor code.
Sure its less characters to type out, but what a foot gun it can grow into when your control flow doesn't get the explicit layout it probably should have gottenCLOVIS
05/13/2025, 3:32 PM?: run { … }
that cause bugsCLOVIS
05/13/2025, 3:33 PM?.let {}?.let {}?.let {}
where a null
happens in an unexpected place and the rest of the chain silently doesn't execute at all. Or a ?.let {}?.let {} ?: error("x not found")
where the error message is x not found
even though the thing that was nullable was an entirely different thingtrevjones
05/13/2025, 3:40 PMfoo()?.bar()?.baz()
or the let
blocks in your example change slightly its hard to spot that the meaning of a given null return may have changed and not mean what was originally represented by null.trevjones
05/13/2025, 3:42 PMy
05/13/2025, 4:03 PM?: run { }
explicitly avoids null chaining, functioning as a clearly demarcated fallback case. I think it's fine as long as you don't chain more scope functions or elvis operators to this run { }
.
if you're okay with using ?:
at all then you trust the programmer to be able to understand if/when that closure is invoked. (and how else would you handle passing a block to the elvis operator instead of an expression, when Kotlin doesn't support block expressions?)
I think the issue mentioned with null-chaining ("`foo()?.bar()?.baz()`"), is that adding or omitting a ?
can lead to unexpected short-circuiting. if not by you then by the next guy editing this codetrevjones
05/13/2025, 4:10 PM?: error("bad thing happening")
or someJsonData?.foo?.bar
to unwrap stuff to pass another function. most concise label i've been able to think of is calling it nullability chain control flow
.
really just a gut feeling measure of looking at the code and trying to determine if the nullability details are doing any heavy lifting. needing a scope block of code on an elvis operator is a dead giveaway that the null details are probably lifting more than they should be.
very subjective. But tends to treat us better when you have 40+ people having to touch the same code over a few years.y
05/13/2025, 4:17 PMtrevjones
05/13/2025, 4:23 PMy
05/13/2025, 4:40 PM