I would prefer the first approach with nested calls, because the actual control flow (what result is passed where) is much better visible. If readability is your goal, I would introduce variables with a meaning:
val average = actionOne(obj)
val power = actionTwo(average)
val cost = actionThree(power)
This would add a meaning and not just superficial cosmetics.
Just wanted to point out that you can use function references and drop the lambdas and the it. The syntax for the function references can vary based on where the function is defined. In the simplest case that would be:
val obj = SomeObject()
obj.let(::actionOne)
.let(::actionTwo)
.let(::actionThree)
That’s not always true and one of the big problems with Kotlin function reference usage/resolution. If those functions had default params for instance, it breaks. There’s no mention of the function signature so we shouldn’t assume.
Probably not too relevant to all this pipe forward, functional programming discussion that keeps happening, but when I think of function composition mathematically, I think of:
f(g(h(x))) = (f ∘ g ∘ h)(x)
I don’t know what syntax would best encapsulate this kind of mathamatical formatting, but it’s worth putting out there.
When did I say it was always true? The OP showed 2 alternatives and I simply showed that there is a third alternative using function references. The ability to use function references is a point in the favor of the let alternative and why I prefer it.
That style also works well if for example actionTwo was a member or extension function instead of a function that took a parameter. You can stay in that style and do:
val obj = SomeObject()
obj.let(::actionOne)
.run(SomeType::actionTwo)
.let(::actionThree)
Just saying, you can’t go running around telling people the lambda can be replaced with a function reference without knowing the function signature just like the IntelliJ inspection limitations (and many times it won’t tell you it can be replaced because the signature doesn’t allow it).