Don't you guys sometimes stumble across this patte...
# getting-started
d
Don't you guys sometimes stumble across this pattern
Copy code
computeIt().let { if (condition()) it.enhance() else it }
which would benefit from having dedicated
letIf
construct? Am I missing something or it's just one of the many cases which just isn't worth adding to the stdlib?
โž• 1
r
I think it's to prevent 'polluting' the stdlib indeed. While it would be very convenient for me to have as well in some cases, I also needed something like
.filterIf(condition) { ... }
on a collection a few times. And there are most likely more candidates, so why implement it for one if not for the other? Which leads to multiple needed functions which can also indeed be easily created by yourself when needed ๐Ÿ™‚
d
Well, following this reasoning we wouldn't even have the standard scope functions ๐Ÿ™‚ But I understand that the boundary must be defined somewhere.
r
what I mean is, if they would add
alsoIf
, someone else will ask for
filterIf
(maybe me ๐Ÿ˜‡), next thing you know, everytime a library function is added we would need to consider the
libraryFunctionIf
variant, which is a pollution IMHO. Especially since it's very easy to create for yourself when needed. ๐Ÿ™‚
d
I understand but - to continue in this line of arguments - we have a "orNull" variant for almost all functions so having an "if" variant everywhere seems quite a logical, yet still generic enough step.
a
I think your example might be missing something, I get a warning because
it
is unused :)
d
@Adam S That's probably because you are not using the return value.
a
the result of the lambda of
also
is never used, since it returns Unit
r
should probably be
.let
instead of
.also
indeed ๐Ÿ™‚
๐Ÿ‘ 1
โž• 1
a
this is why I try to avoid the scope functions because I get confused with them all the time ๐Ÿ™ƒ
๐Ÿ˜… 1
d
(it was just a typo. In reality I'm using
run
in my particular case ๐Ÿ™‚ )
๐Ÿ‘ 1
a
thanks, it doesnโ€™t detract from your point but I wanted to play around with it and make sure I had the right example
r
regarding the
orNull
, true, and probably one of the Kotlin stdlib developers can provide the correct arguments here, but my assumption here is that they are added because of the explicit null-safety features/guarantees provided by the language. So if the stdlib would only provide the non-nullable variants, developers would need to catch exceptions for the null/empty collection variants, and if only the
orNull
variants were present, developers would need to explicitly cast to non-nullable types wherever they actually know it will never be null. but in general, it's indeed just a matter of where to draw the line. I agree with you that it would be nice to have the
...If
variants present in the stdlib, but I'm not the one needing to maintain them ๐Ÿ˜
a
I think this conversation is quite close to the question of โ€œwhy doesnโ€™t Kotlin have a ternary conditional operator?โ€ The short answer being: because if-statements do the same thing (even if they are slightly more verbose). However, if-statements prevent a bigger problem that Iโ€™ve had the misfortune of seeing - multiple ternary conditions https://stackoverflow.com/a/4986698/4161471 which even when very simple are a nightmare. On the other hand, if an if-statement needs to be modified to add another branch, then itโ€™s still going to look fairly sensible, or at least robust enough to refactor into something more reasonable. So, I wonder if adding
..If
variants would open the door to a similar mess? Itโ€™s probably okay using one
letIf() {}
, but what happens when lots of them are chained, or nested? And combined with the (imo) already confusing scope functions? I suspect it wonโ€™t be very legible, and just using an if-statement (or when-statement) would be better - but thatโ€™s just a gut feeling.
d
Good point but actually this enhances my desire to have
letIf
even more because for my way of thinking it's much more natural to chain the conditions then combine them. E.g. I would rather have
Copy code
value.letIf(cond1) { ... }.letIf(cond2) ...
then
Copy code
value.let {
  val uselessIntermediateVal = if (cond1) {
     ...
  } else {
    it
  }
  if (cond2) {
    ...
  }
}
j
In general, I find that higher level concepts are better extracted into functions, so I almost never have to use
let
. What is your real-life use case here?
a
Yes agreed, that
value.letIf(cond1) { ... }.letIf(cond2) ...
example looks quite fluent! However I would also probably prefer to make a specific function. Even a local function can help out enormously.
Copy code
fun computeResult(
  enhanceFooEnabled: Boolean,
  enhanceBarEnabled: Boolean,
): Float {
  fun Float.enhanceFoo() = if (enhanceFooEnabled) enhance() else this
  fun Float.enhanceBar() = if (enhanceBarEnabled) enhance() else this

  val value: Float = computeIt()
  
  return value.enhanceFoo().enhanceBar()
}
d
@Joffrey My real life example is the one I started this thread with. It's just a simple condition inside
let/run
which seems unnecessarily verbose, given how compact yet still clear Kotlin usally is. I don't have a concrete example for the chained
letIf
yet. That was just an extra argument for me why something like that should be in the stdlib.
j
You mean that in real life you have a function named
computeIt()
and a function named
condition()
? My point is about semantically extracting a function out of this construct, but without real life names this is moot. Usually something like this is more readable:
Copy code
computeIt().enhanceIfNeeded()

fun TheType.enhanceIfNeeded() = if (condition()) it.enhance() else it
But maybe the code can be organized differently as well, and not even need that
d
Okay, so my real code is this
Copy code
mergeToBalanceHistory(runSumTransactions, aggregatedBalances)
            .run { if (isGroupByDate) fillMissingPeriods().applyDateFilter(dateConditions) else this }
It's IMO exactly the same pattern. I don't see how extracting to an extension function would help my main problem which is the verbose
if (cond) doSomething(it) else it
pattern. Of course, the verbosity here is not that bad after all. I was just interested how the community views this problem in particular, and the extension of stdlib in general.
j
Do you assign the result to a variable? Why do you need
.run
and
else this
if you don't? If you do, then this whole expression could actually be in a function using early returns, and there is no need for scope functions
d
I think we are now getting to the territory of purely personal preference ๐Ÿ™‚ I just don't like early returns. Besides, that option would need an extra val, and naming things is hard, so I found out that sometimes it's just clearer to work fluently with "anonymous" objects than to invent bad names.
j
I think we are now getting to the territory of purely personal preference
Of course, I agree. It seemed to me that the question is all about style anyway, so I figured it's got to have personal preferences involved.
Besides, that option would need an extra val, and naming things is hard
Yes, and this is precisely why it is important to have these intermediate variables and visible names. Naming is hard because it forces the author to think more, so that the reader can think less. I'd rather do that work at writing time given that we write way less often than we read. Sometimes, it's not that hard, though ๐Ÿ™‚ But when it is, it's all the more important IMO. If it's super hard, sometimes it points out that we didn't break down into variables at the right moment, so it helps restructure.
d
Agree with all your points re naming. But as you said sometimes the naming wouldn't bring additional readability value, just extra verbosity, so that's when I usually employ scope functions.
For example in my case I could easily have
Copy code
val mergedBalances = mergeToBalanceHistory(runSumTransactions, aggregatedBalances)
But does it bring greater clarity? IMO not
j
Yes it does. I would argue that scope functions in big expressions increase the cognitive overhead quite a bit, as opposed to using a (maybe redundantly named) simple local variable. Using this in the next expression makes it a breeze to read, as I don't have to parse with my eyes the complexity of the
.run { }
โž• 1
Sometimes shorter is not more readable, and I strongly believe this is the case here
I have read a lot of "smart" code, and I can assure you that I strongly prefer reading "stupid" code that's 1 line longer
๐Ÿ’ก 1
โž• 1
d
Again, I agree with you on all points. It just depends on what you consider "big expressions" etc. Also, it's partly what you are used to. If you are coming to Kotlin from plain Java you might be quite intimidated by scope functions in any context, thinking that it's just unnecessary display of "smartness". Does that mean that scope functions are useless/harmful? Probably not...
j
It just depends on what you consider "big expressions" etc
True, but maybe we can agree that if you had to wrap the expression over 2 lines, it can be considered "big"
Also, it's partly what you are used to. If you are coming to Kotlin from plain Java you might be quite intimidated by scope functions in any context, thinking that it's just unnecessary display of "smartness".
I totally agree. Though after coding in Kotlin for about 5 years, and using scope functions here and there, I still very often find variants without them to be more readable (unless the expression is extremely simple). Admittedly I'm big on extracting functions, so I rarely am in a situation where I need a scope function in the first place.
d
I think that in the end it might also be a matter of personal experience or way of thinking. Language (especially a general purpose one) is just a toolbox and everyone eventually finds his own unique way to use that toolbox. What's IMO important is to understand the toolbox well and know when and how to use each particular tool, so that you are able to build something with it. I hope that these kinds of discussions enhance our ability to use those tools ๐Ÿ™‚