Pipe-forward operator |>

Programming can get messy. So messy in fact that function calls can get so embedded that they become difficult to follow.
Take the following nested functions into consideration:

getResponse(notify(persist(validate(request))));

Pipe-forward operator lets you pass an intermediate result onto the next function.
Let’s take another look at the code snippet above rewritten using the pipe operator:

request |> validate() |> persist() |> notify() |> getResponse();

Also it will be useful for Railway Oriented Programming.

12 Likes

I think for the cases this is needed, the following might be close enough:

fun main(args: Array<String>) {
    val r = "hello" next ::length next ::double
    println(r)
}

fun length(s:String) = s.length
fun double(i : Int) = 2 * i
infix fun <T, R> T.next(map : (T) -> R) : R = map(this)
5 Likes

Yeap, very cool for one line, but you cannot do like this:

request 
|> validate() 
|> persist() 
|> notify() 
|> getResponse();

Multiline is more readable.

2 Likes

You can find this operator in such languages like: F#, Swift, Elixir, Unix shell.

That’s why I said “close enough”. I think the added value for such feature can be considered minimal while it adds substantial complexity to the language.

Looks like you are talking about Functor (see e.g. Haskell).

A functor is defined upon it’s context and leaves it context intact. E.g. a map function on a List class results in a List with transformed values, the context List remains the same. This is “just” applying a function.

Back to the topic, I think adding new operators to the Kotlin should be done rarely. If you do fancy all kinds of operators there are already plenty of languages that fill that spot, with Scala being the obvious one. I agree with sodiaan that, when familiar with pipe-forward operators, this example is very readable. But omitting parameters can be considered as unreadable too.

I find the following almost just as easy to read (and write) without the need for any special language constructs.

val v = validate(request) 
val p = persist(v) 
val n = notify(p) 
return getResponse(n);
1 Like

Exactly! And in a former problem:

request |> validate() |> persist() |> notify() |> getResponse();

We actually have such a context but hidden, it is something like IO monad.

But of course I do not agitate to introduce monads into Kotlin, but just want to emphasize that the problem bay be generalized in some way to make |> or other like that more general mechanism in Kotlin,

What kind of complexity it adds to language?

1 Like

Here is the gist with some not very nice implementation of pipe forward operator(unary plus in a gist), it is steel required to have some aid from Kotlin lang designers to make it elegant in usage.

In short, we need an operator ‘|>’ which actually converts a:

(T) -> T1

to:

<T>.() -> T1

and have a nice syntax support for usage in a Kotlin lang because it is not very nice to see it as:

request.(+::validate)().(+::persist)().(+::notify)().(+::getResponse)()

I am trying to say, what if to make it possible for any function be used as an extension function? That is if we have:

fun func1(a1 : T1, a2 : T2, ..., a_n:Tn) : Tz

then it would be also seen as:

fun T1.func1(a2 : T2, ..., a_n:Tn) : Tz

In such a case it would be even possible:

 request
. validate(someArg1) 
. persist(someArg2, someArg3) 
. notify() 
. getResponse();
6 Likes

Every feature adds complexity. It’s just another construct we need to learn, be able to read and write and of course a compiler needs to compile.

2 Likes

I like the idea of having the ability to call any function like an extension. There are plenty of Java libraries with classes containing static utility methods which may be called like this.

In Kotlin, this is usually achieved by declaring extension functions and using library combinators such as let, apply, and run.

request
.let { validate(it) }
.let { persist(it) }
.let { notify(it) }
.let { getResponse(it) }

Still, there is a difference between functions with receivers and functions without receivers, and it can get quite messy:

request
.run { validate() }
.apply { persist(this) }
.let { notify(it) }
.run { getResponse() }

(in fact, I hit that issue in F# code multiple times: F# |> works with free functions, but not object methods).

So, yes, some sort of “unified function call” operator might me helpful.

10 Likes

I suggest to use the unicode sign “phone” (since phones are used to call) for it: ✆ or :phone:

:slight_smile:

9 Likes

In addition to @vjache’s comment it would be great to have some option to import function as an extension. I had this idea for a long time. As soon as I read the reference I constantly feel a lack of such functionality. Unfortunately I’m not a Kotlin user, just a fellow traveler. As for me let constructs are not elegant in such cases.

But of course there are some issues to be discussed about like:

  • allowing not only the first argument to be treated as receiver;
  • selective import of overloaded functions;
  • import with wildcard;
  • etc.

Maybe I’m wrong, but you can do that by creating a little (inline) non-member helper function that makes the right calls. Some of the issues with imports (where it is limited) could be handled the same way.

1 Like

If we do ANYTHING along these lines, I recommend going vjache’s route, especially since plain functions and extension methods are both just implemented as static methods in Java. In reality, the only difference between a function call and a method call is how you pass in the primary parameter.

Now, personally, I don’t think this needs to be implemented in any way at all. If we really need to string along some function calls, we can make our own versions as extension methods and chain dot operators. We certainly don’t need a pipe operator.

1 Like

Why is it so complex to parse the first parameter as this-instance?
Isn’t this what Java internally actually does, passing this as first parameter and accessing it via byte-code instruction aload_0?

1 Like

This would be a backwards incompatible change, because it would create overload resolution conflicts or change the way overloads are resolved in cases when you have a method and an extension function with signatures differring by one parameter in Kotlin 1.0 code.

3 Likes