George
07/26/2024, 8:50 AMCLOVIS
07/26/2024, 8:58 AMfun example1(): Node? = Node().let(Util::process)
vs
fun example1(): Node? = Util.process(Node())
is the first example really better code, here?CLOVIS
07/26/2024, 9:01 AMTheNo, that'sscope function shines the most when it is used for simple one-line side effects..let
also
. also
is used for side-effects, let
is used for pure transformations.
You can see this by looking at their signature (simplified):
fun T.let(block: (T) -> T): T
fun T.also(block: (T) -> Unit): T
let
is T -> T
, meaning that its return value is passed to the rest of the computation. also
is T -> Unit
, meaning that the lambda returns no useful information (thus it must have done something else, which is a side-effect).
You can remember this by comparing it with lists:
fun List<T>.map(block: (T) -> T): List<T>
fun List<T>.forEach(block: (T) -> Unit): List<T>
e.g., if you would use map
with a list, you should use let
; and if you would use forEach
, you should use also
.CLOVIS
07/26/2024, 9:05 AMfun example2() {
val scope = createScope().let { it.name = "test3" }
// ... more code
}
Here, the type of scope
is Unit
, it's not the result of the createScope
function. In fact, the returned value of createScope
is lost and garbage-collected at the end of the line. This is because assignments are not expressions, so the type of it.name = "test3"
is Unit
. Since let
is used for pure transformations, the result value is Unit
.
If you had used also
in this example, then scope
would indeed contain the return value of createScope
.CLOVIS
07/26/2024, 9:08 AMfun example7(): Node? {
// ... code
// ... multiple lines of code
return Node().let { process(it) /* returns new node or null */ }
}
Without scope functions (your version):
fun rewriteExample7(): Node? {
// ... code
// ... multiple lines of code
val node = Node()
val newNode = process(node)
return newNode
}
Without scope functions (my version):
fun rewriteExample7(): Node? {
// ... code
// ... multiple lines of code
return process(Node())
}
CLOVIS
07/26/2024, 9:10 AMGeorge
07/26/2024, 9:24 AMCLOVIS
07/26/2024, 9:27 AMThough I have seen it being used as a side-effect and tbh I do not mind it, in case you do not care about the result ofcBut it's not meant for that, that's
also
's job. If you're using let
to do this, you will have bugs where the wrong value is returned—and there was one in your examples.
There are so many scoping functions because each one has a specific role. Using them with the wrong role works most of the time, but it makes code less readable (because you don't know if it's used correctly) and it can introduce bugs.Klitos Kyriacou
07/26/2024, 10:25 AMreturn Node().let {
swap(it)
it
}
can be shortened to
return Node().also { swap(it) }
CLOVIS
07/26/2024, 10:41 AMNode().also(::swap)
🙂George
07/26/2024, 11:18 AMGeorge
07/26/2024, 11:20 AM.let
for side-effects. In case you want to explicit return Unit, then it is ok I believe. Wdyt?
fun explicittest2() {
scope.let { it.name = "test3" } // returns Unit
// ... more code
}
CLOVIS
07/26/2024, 11:31 AMfun foo() {
scope.name = "test3"
// ... more code
}
George
07/26/2024, 11:32 AM?
probably in all cases it wont make senseCLOVIS
07/26/2024, 12:02 PMlet
should be used is when calling a single conversion function (e.g. it.toDto()
). In all other cases, I avoid using it, code is almost always simpler without itCLOVIS
07/26/2024, 12:03 PMalso
because it doesn't impact data flow so it doesn't make the code harder to debugYoussef Shoaib [MOD]
07/26/2024, 12:07 PMfun T.let(block: (T) -> R): R
Which tells you that its return type has nothing to do with its input, which is good! It tells you that you can change the return type with it!CLOVIS
07/26/2024, 12:08 PMmap
, the rest of my arguments still stand 🙂