Pipe-forward operator |>

val pipeline = listOf(
  ::sanitize,
  ::validate,
  ::parse,
  ::process,
  ::optimize,
  ::publish
)

val result = pipeline.fold(input) { output, stage -> stage.invoke(output) }

// You can hide this building block away behind a function
6 Likes

Nice idea @oluwasayo!

fun <T> pipeline( vararg functions :(T)->T ) :(T)->T = { functions.fold(it) { output, stage -> stage.invoke(output) } }
3 Likes

Don’t the solutions proposed here which involve let/apply/run, as well as the more exotic ones, carry huge performance overhead?

And for such a basic construct as calling functions.

(side point: new version of IDEA emits warnings on using let with a single statement)

A simple approach with treating |> as a syntactic sugar at the compiler level, and just rearranging function calls, is much more effective.

Those functions are declared as inline functions. Using them is not less efficient than inclining their functions bodies.

The warning for single statement let is supposedly just for reducing verbosity where possible.

3 Likes

Good to know! I have been using them a lot recently :smile:

@fvasco, to give your extension of @oluwasayo’s idea a concrete example:

val result = pipeline(
  ::sanitize,
  ::validate,
  ::parse,
  ::process,
  ::optimize,
  ::publish
)

This is indeed pretty nice and would avoid the uglyness of |> (which could easily win the “ugliest Kotlin operator” award). Plus, pipeline would be self-explanatory, whereas |> looks like some Perl programmer came to life again.

4 Likes

That one only works when the type never changes throughout the pipeline. For a general solution, you would need an overload for every possible number of functions:

fun <T0>         pipeline()                                : (T0) -> T0 = { it }
fun <T0, T1>     pipeline(f1: (T0) -> T1)                  : (T0) -> T1 = { f1(it) }
fun <T0, T1, T2> pipeline(f1: (T0) -> T1, f2: (T1) -> T2)  : (T0) -> T2 = { f2(f1(it)) }
/* insert 253 more of these */
3 Likes

The newest kotlin plugin will raise a warning in code inspections

This inspection detects a redundant let when it includes only one call with lambda parameter as receiver.

Why could this happen?

a.b?.let { it.c } can be simplified to a.b?.c

I mean the chained function calls with lets suggested by @Dmitry_Petrov_1 will raise this warning

Same reason, they can be simplified.

Thank you @Varia,
this should work better

    infix fun <A, B, C> ((A) -> B).then(other: (B) -> C): (A) -> C = { other.invoke(this.invoke(it)) }

    val composition = ::sqrt then ::println
    composition(16.0)
3 Likes

Hello,
Wondering what’s the current thinking within JetBrains about the pipe operator.

Looks like pipe operator might be coming to javaScript soon, this is the current proposal: https://github.com/tc39/proposal-pipeline-operator

It is already supported by Firefox: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Pipeline_operator

Kotlin extension functions do alleviate the problem (especially let, but many, many others e.g. anything that operates on collections) but they don’t fully solve it.

In my opinion, the pipe operator is as essential to FP as is dot operator for OOP.

I do realize that, from where Kotlin is now, it might be painful to turn back and embrace that piping is better than extension functions. But on the other hand, I feel like staying on this course will make Kotlin lag behind JavaScript/TypeScript in the long run.

Any comments are welcome!

I don’t agree. They are both solutions to the same problem. The only difference is that with extension functions the function author decides how the function is called while with piping you can call it in 2 different ways.
This might seem like a good argument for piping (more flexible) but this also comes with the cost of a language which is now harder to learn. Extension functions on the other hand are self explanatory.

That all being said I think there was an example in here, with a nice version of an in language implementation of pipelining

// credits medium , fvasco  and oluwasayo 
fun <T> pipeline( vararg functions :(T)->T ) :(T)->T = { functions.fold(it) { output, stage -> stage.invoke(output) } }

val result = pipeline(
  ::sanitize,
  ::validate,
  ::parse,
  ::process,
  ::optimize,
  ::publish
)

In the end it just comes down to API design. If you decide you are using Kotlin you should design your API with that in mind. This means using extension functions where they fit. Yes, in other languages you would design the API in a different way, but this does not make one way better than the other.
This topic is now over 45 messages long and I have not yet seen a single example where piping would be better than extensions (this is also true the other way round, although I think that extensions functions are great for other problems than just pipelining).

1 Like

While I would be happy to use the pipeline function shown above or the pipe function shown in this article: https://medium.com/@krpiotrek/kotlin-pipeline-syntax-191b59429c38 my problem is that the type changes in my pipeline - is it possible to get a pipeline or pipe extension function for that case?

Got a version that I am quite happy with based on extension functions that ends up being of shape: class.pipe(::toString).pipe(::toInt).pipe(::toDouble).end(::handleResult) that can handle different ingoing and outgoing types, as well as lists and functions returning nothing. Just the new experimental type inference algorithm is not happy with it. See https://youtrack.jetbrains.com/issue/KT-33325?project=kt

And how is that any better than using the existing library extension function?

class.run(::toString)
    .run(::toInt)
    .run(::toDouble)
    .run(::handleResult)

Yeah, I was not very specific, but it has a bunch of logic inside to implememt rop: https://fsharpforfunandprofit.com/rop/

I do also find the name “pipe” makes it a bit more clear what it does but I guess that is very debatable.

i think the only realistic implementation for this (and the one i’d actually like), is to turn let into an infix operator fun with the operator |>.
After that, it would just need special-casing the |> operator in a way that says "imply a lambda from |> until either newline, |> or parenthesis context change.

this would allow the following:

Person("tom", age = 12)
  .let { findFriends(it) }
  .let { storeFriendsList(it) }

// would turn into
Person("Tom", age = 12)
  |> findFriends(it)
  |> storeFriendsList(it)

which could be called more readable. idk if this is actually a good idea (.let does fulfill pretty much 100% of the use-cases for |>, just without currying,… but kotlin doesn’t curry function calls, so “pretending” to curry by using function-reference-syntax in a different way is ugly too.

i think if |> should be added, it should be as a special-cased operator fun of .let

You can literally do all of this using extension functions. Adding a new operator would be just littering the language with a lot of useless features that can be easily avoided by just writing the extension yourself (which probably takes like 5 seconds when you get used to it) or just by providing an IDE action that creates the extension for you at the bottom of the file. Adding a lot of new language features that can be easily replicated with no overhead is just increasing the amount of things that people need to learn in a way that can make the language inaccessible.