Pipe-forward operator |>

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: GitHub - tc39/proposal-pipeline-operator: A proposal for adding a useful pipe operator to JavaScript.

It is already supported by Firefox: Expressions and operators - JavaScript | MDN

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!

1 Like

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).

2 Likes

While I would be happy to use the pipeline function shown above or the pipe function shown in this article: Kotlin pipeline syntax. Transforming your code into a concise… | by Piotr Krystyniak | Medium 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: Railway Oriented Programming | F# for fun and profit

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

2 Likes

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.

not adding features that improve the language make it either stagnate (-> java) or just stay incredibly limited forever (-> go).

you CAN do this using extension functions, but you should always think about expressivity more than just about “can you do that”. creating an extension function for something that is in no way actually either a) a transformation of the receiver, b) an action the receiver does, c) a check for something about the receiver - imo is a bad idea. There is also the case that there are many methods of other classes you might want to include in such a chain.g

imagine seeing code like this:

val parseableData = "    a: 12, b: 13, c: 'asdf'"
websocketConnection.send(
  someSerializer(
    serializationConfigArgument, 
    myDemoParser.parseString(parseableData.trim()).doSomeStuff()
  )
)

vs

val parseableData = "    a: 12, b: 13, c: 'asdf'"
parseableData.trim() 
  |> myDemoParser.parseString 
  |> it.doSomeStuff() 
  |> someSerializer(serializationConfigArgument, it) 
  |> websocketConnection.send

vs your proposed way, which looks realllllly strange:

val parseableData = "   a: 12, b: 13, c: 'asdf'"
parseableData.trim()
  .parseUsing(myDemoParser)
  .doSomeStuff()
  .serializeWith(someSerializer, serializationConfigArgument)
  .sendToWebsocketConnection(websocketConnection)

The reason this last one looks so strange is that sendToWebsocketConnection is NEVER a method that should be on String, no matter the circumstances. While it does read simmilar to the second one, it does not convey the actual relationship between the classes in an expressive manner.

And that is imo what kotlin is so extremely good at. Expressivity.
So a feature (which is, btw, also proposed for arguably the most popular language, JS, and will most likely get implemented into the JS standard in the not so distant future) that does help readability, expressivity and conciseness should at least be very well considered.

Kotlin is not Go, where features are the devil and language-complexity is something to avoid by all means. Sure, kotlin should stay approachable and not include overly complex features (implicit conversions come to mind), but this is not one of those. kotlin does already have an incredibly large feature-set for such an approachable language. adding features is not a bad thing. Staying verbose and unexpressive however, is.

4 Likes

The pipe operator in Kotlin is an important feature that could consolidate the language for data science use especially in exploratory data analysis. As we mentioned before. It is not a question of what you can do. Kotlin is used in data science already but the pipe operator will increase its expressivity for data transformation pipelines such as:

dataframe
   |>filter( lambda expression )
   |> orderBy(["age"])
   |> select(["id", "name"])
   |> plot()

Additionally we already have data science kotlin libraries inspired by functional languages. This operator could improve how we express such transformations with them. Some examples are krangl (based on dplyr) and kravis (based on ggplot).

Please note that the pipe operator described above is not exactly like Unix pipes or C++ functors.
It much more similar to a macro or replacement that will replace the argument before the pipe (left) as the 1st argument of function after the pipe (right). That is how is works in R (perhaps in f# as well).

Therefore a function like:

head(dataframe)   // fun head(dataframe: Dataframe) { .. implementation .. }

Can be called as:

dataframe |> head()

This way library designers can implement a series of dataframe transformations as functions whose 1st argument is a dataframe. (look at dplyr). The subsequent arguments can be made optional with default values.

1 Like

And once again what does that provide over:

dataframe
   .run { filter( lambda expression ) }
   .run { orderBy(["age"]) }
   .run { select(["id", "name"]) }
   .run(::plot)

But that being said having worked in Dart I did like .. cascade operator which is sort of an equivalent to apply

You don’t even need the .run bits. Kotlin has receivers, that provides this capability.

1 Like

I know you could change the API to be fluent, but was assuming doing it without changing the API and assuming that API was not fluent.

My response to comments that say “you can do this with let and run” … honestly can we not see the additional syntax and number of extra characters? Brevity and elegance are not for show. At scale, in real world programs, brevity makes for fewer mistakes. I feel this shouldn’t need to be said.

1 Like

Do you have done evidence that supports that conclusion, since that is not my experience? I find that clear, readable code does that not striving to use cryptic syntax to reduce the code by a character or two and do not feel that needs to be said

3 Likes

This is as far as I got with an infix function, featuring classic functional paradigms like partial functions and function currying. If it weren’t for backticks, it would look pretty nice in my opinion.

typealias id<T> = (T) -> T
typealias rec_bin<T> = T.(T) -> T

inline infix fun<T> T.`|`(fnc: id<T>) = this.let(fnc)
inline infix fun<T> id<T>.`|`(crossinline fnc: id<T>): id<T> = { this(it) `|` fnc }

fun<T> partial(op: rec_bin<T>, operand: T): id<T> = { it.op(operand) }

val addCinque = partial(Int::plus, 5)
val negative = partial(Int::times, -1)

fun main() {
    val pipe = addCinque `|` negative
    println(4 `|` negative `|` pipe)
}

But as long as such a pipe forward operator can be consider usable, I personally would not advocate modifying language syntax and its parser accordingly. As it’s been already mentioned in this thread, you can use let (or run) which does the same job and seems to be as concise as a dedicated operator.

1 Like