Noisy Code šŸ—£ With Kotlin Scopes

Chetan Gupta
ProAndroidDev
Published in
6 min readApr 4, 2021

--

Scopes make your code more readable? think again

You are going to encounter these scope functions namely let, run, apply, also, within every Kotlin codebase, along with all the mischievous ways developers exploit their usage from the way they were intended for. Let see how popular opinion on those ends up just as a code noise.

Featured On

Let me out! ā›“

Popular opinion ā†’ let is treated as a native way to do null checks on the variables.

Please donā€™t do that even if using `let` feels functionally beautiful but

  • You will end up creating another reference called it in the same scope which is less descriptive
  • To fix that you will definitely try to provide an explicit parameter for lambda example imageFile?.let{ image ā†’,
  • but it creates a load of finding another name for the same variable (naming things are hard)
  • Now other devs can refer to image files in the scope with two names imageFile and image, with time you need to maintain that the right variable is being used inside the scope of lambda when there is iteration over the code for bugs and changes.

We can avoid these and other hiccups just by doing normal null checks.

And for guys who are too smart will argue that smart casting wonā€™t work on global fields. I will say read my article šŸ‘‰ [lateinit vs nullable types | global fields] or watch my video šŸ‘‡

Video is in Hindi

And stop polluting your global space.

The reason why smart cast fails because the global field can be accessed by other functions of the same class. In any async situation, global value can update at any time so itā€™s impossible to smart cast.

So, to fix that you can use

Since you will end up making a new variable anyways which might be of a different name, you can get away with using let in such cases.

PS: donā€™t forget to make it descriptive

Another Bonus part ā€¦

  • Nesting scoped function is considered bad practice even Kotlin style guide point this out.
  • Prefer using if for null checks then you can use scope operators where it is actually required.

Community Feedback

Thanks, Joost Klitsie Provides better alternates regarding the use of let, I definitely loved all the approaches.

// alternate 1 
fun deleteImage() {
val imageFile = getImage() ?: return
...
}
---- OR -----
// alternate 2
fun deleteImage() {
getImage()?.takeIf { it.exists }?.let {it.delete()}
}
---- OR -----
// alternate 3
fun deleteImage() {
val image = getImage()?.takeIf { it.exists } ?: return
image.delete()
}

In this way avoided assigning a new value to a variable in the function scope, so It do not have the double name problem.

Also, Apply my rule! šŸ‘‘

Edit 1: This section has been updated after the feedback from Joost Klitsie.

Popular opinion ā†’ Create scope to isolate common functionalities to modularize your code.

The pure functions|lambdas generally are expected to be one-liner expression. This has been explained really well by Sir Venkat Subramaniam in one of his talks about functional programming, highly recommended to consume all of his work!

TLDR

  • Lambdas shine out in making code concise by still retaining the expressive parts of it.
  • Generally, lambdas with huge function bodies end up hindering the readability of the functionally chained pipeline you want to create.

So chaining with small lambdas doesn't disrupt the readability of your code.

From the above example

  • The scope of intent is limited inside also, the intent is restricted to that lambda itself
  • This will make you add all the code that needs to access intent into the same scope
  • It will not take a lot of time when also lambda becomes a huge mess

A simple fix to it ā€¦ Flatten your hierarchy when possible

Flattened and not scary anymore, there was no point in making another scope introduce another scope for just consume the object.

Community Feedback

Thanks, Joost Klitsie for your insights. Your example on abstracting out into the function is really clean, definitely works best to control chaining.

fun startSomeActivity() {
startActivity(getSomeIntent())
}
fun getSomeIntent() = Intent(context,SomeActivity::class.java).apply {
//....
}

Run from Run ?: šŸƒšŸ»ā€ā™‚ļøšŸ’Ø

There is two run in Kotlin one is a scope/extension function and the other is a normal higher-order function.

popular opinion ā†’ Itā€™s commonly used to execute some statement.

Wtf is this?ā€¦ absolute disappointment I suppose.

  • Absolutely yes you can chain scoped operators like this but should we do it? NO!
  • Not many devs are aware that there are two types of run in the language and having both of them chained using Elvis operator is a complete disaster.
  • It adds no value to the code, just branching and noise

And smart devs who are thinking that this pattern might be useful when you have more line of code there instead of the one-liner, go and read Apply my rule section again! else itā€™s a democratic country do whatever you want.

Community Feedback

Joost Klitsie advice to come up with some helper extension functions you can use throughout the project, or use Kotlin native one.

preferenceManager.getInstance()?.getName().orDefault {
Log.w(ā€œName not found, returning defaultā€)
getString(R.string.stranger)
}

Which really is a good way to architect your code, here the point of focus is to apply a pragmatic approach instead of chaining scopes mindlessly. Use your tools wisely.

You are not With me šŸ˜­

The most ignored one, and not an extension function.

Devs often ignore with() for two reasons

  • It takes receiver in parameter, not in extension, so people canā€™t do function chaining with using it.
  • Secondly, doesnā€™t go well with nullable types receiver

Thatā€™s really sad, with is a really useful scope operator. IMO, it has very special a use-case, cause it's not good for receiving parameter and make function chains but it is very useful when you want to set the scope of the variable into the entire function body, see this example

If you compare it with other scoped operator example :

Anyway this ugly is an example of how to cater to nullable types as well, you can use run with nullable here, but I will highly suggest not to pass nullable parameters into your functions.

Community Feedback

Joost Klitsie advice to use apply , as itā€™s not returning the run result. That would work better than run absolutely agreed.

Conclusion šŸ’†šŸ»ā€ā™€ļø

In isolation using these extensions does look sensible and pragmatic but as whole they are not readable or maintainable, we lose the intent of what we wanted to modularize and so many blocks make it really hard especially in code reviews, always leverage Kotlin features where they do good.

Hope you find it informative and if you have any feedback or post request or want to get in touch, I'm always available on my social media all links below

If you liked my content and want to read more check out my awesome blogs!

Check what Iā€™m upto

Iā€™m an active member of an OpenSource Organisation called theCodeMonks, do check out the impact and knowledge we are sharing with communities

Do consider Clap to show your appreciation, until next time. Happy Hacking! šŸ‘©ā€šŸ’»

Read Next

--

--