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 AMrunCLOVIS
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