Pipeline operator?

This summer someone proposed adding the operator in Haskell. The discussion was very involved: Add the ‘|>’ pipe operator

TLDR:

Altogether we have 2 votes in favor, 3 votes against, and chessai is on a temporary leave (#82). Even if he votes in favor, we still fall short of 4 votes required to approve the proposal. Thanks all for the discussion (and foremost to @chshersh), unfortunately, the proposal is declined.

1 Like

@chaotic3quilibrium You are too kind :pray: - I am so glad you found it useful - Thanks.

1 Like

In case this is added, then I hope that the operator |> will still be free for our own DSL’s. For example by making |> available as import extension just like pipe, and not a generic operator on all types by default. That would for example interfere with my DSL, which has operators like |>, |+ etc.

BTW, i have also defined a pipe operator for every day use long ago (probably like many others). The: | (modelled after the bash pipe operator). Imho this is just as readable, and simpler:

val res = 3 | triple | sum(2) | half
1 Like

The | operator is already used to define unions.

Used on types, the | operator creates a so-called union type . The type A | B represents values that are either of the type A or of the type B .

1 Like

In Elixir it’s |> too https://elixir-lang.org

1 Like

Here is a previous discussion: Give The pipe Method of scala.util.ChainingOps An Operator Representation - Language Design - Scala Contributors (scala-lang.org), and I think the first response of it is very useful.

1 Like

The last response is most useful, indeed. That’s a clever idea, for shift-enter in REPL to go into pipeline mode.

I’m not a fan of declaring “you can’t use this feature because it hurts performance unless you use knobs or maybe it doesn’t matter.” There are plenty of cases where JVM performance was affected for various reasons one would prefer not to care about.

import scala.util.chaining._

final class C {
  def s = "hello, world"
  def f = s pipe len
  def len(x: String) = x.length
}

In Scala 2, -opt:inline results in

  public int f();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: getstatic     #27                 // Field scala/util/ChainingOps$.MODULE$:Lscala/util/ChainingOps$;
         3: pop
         4: getstatic     #30                 // Field scala/util/package$chaining$.MODULE$:Lscala/util/package$chaining$;
         7: pop
         8: ldc           #17                 // String hello, world
        10: invokevirtual #35                 // Method java/lang/String.length:()I
        13: ireturn

I think there is also a lever for mitigating module loads?

I’m a big fan of expr.tap(init(_)) where it’s obvious up front what I’m getting and that some side effects happen by the way. Often it also saves braces.

True, although those applications would not interfere i guess. But besides that, i defined the operator long before it was defined as type union and use it all over the place. This can be an issue with other new operators and keywords that are defined later on as well. That people already have them in use for something else. (enum anyone? :wink:)

Anyway, i don’t say the | operator is better or so, the only thing i would like is that new operators are not ‘imported by default’, thereby interfering with DSL’s.

it also defined as |> for Flix and and Elm

some of them like F#, Elm also have the pipe backward <|

In Scala 3, inline in the source takes care of (at least most of) it. I have yet to find a case where the following is slower than writing things out by hand in microbenchmarks.

extension [A](a: A) {
  inline def pipe[B](inline f: A => B): B = f(a)
}
1 Like

we can also transform

val piped = 3
    |> triple 
    |> sum(2) // curried
    |> half

to

val classic = half(sum(triple(3))(2))

at compile time, similar of how the s"Hello, $name" string interpolator is rewritten by compiler

That basically is what happens with inline in Scala 3, except it’s not a special case like the string interpolator.

2 Likes

Hello. I’m very new to Scala, recently coming from F# where the |> operator is prevalent. I’d like to cast my vote against adding the |> operator to Scala because it would be inconsistent with . chaining.

For example, in my opinion:

def distance(a: List[Double], b: List[Double]): Double =
  a.zip(b).map(_ * _).sum.pipe(Math.sqrt)

is more consistent than

def distance(a: List[Double], b: List[Double]): Double =
  a.zip(b).map(_ * _).sum |> Math.sqrt

In F#, the constant battle between the two styles of |> and . was driving me crazy. There is even a library called FSharp.Core.Fluent (that I also contributed to) that adds . methods to the core collections. Instinctively, I suggested a .pipe method for this library even before I knew that it existed in Scala.

The main problem with |> is the lack of IDE support for code completion. This is why, in my opinion, . is a better way to chain methods.

In conclusion, I would not like to see |> in Scala. The regularity of using . chaining is what appeals to me about Scala as a newcomer.

5 Likes

is better than

because it doesn’t require parentheses around Math.sqrt

a.zip(b).map(_ * _).sum.|>(Math.sqrt)

Scala 2 is agnostic about the invocation style (infix vs dotted selection).

Scala 3 wants only symbolic operators for infix, though you can backtick an alnum:

def f = s `pipe` len  // from my previous comment

def check(x: AnyRef, y: AnyRef) = x.eq(y)  // we used to write (x eq y)

It turns out that the old style of omitting dots and parentheses failed to avert global warming and shrink the Great Pacific Garbage Patch.

Since I tend to need them for ease of reading, I use as many as necessary, guilt-free.

But symbolic operators can be tastefully applied. The discussion was about how to indicate that “pipe” is a synonym, in order to make it easier to search documentation. I don’t remember if they invented a mechanism for that. Maybe the alpha annotation.

1 Like

Maybe it would be great to return /: and :/ back to Scala? These operators was really great :slight_smile:

1 Like

I see! I didn’t know about the infix notation. Now that I do, here’s another attempt at the distance function:

(a zip b).map(_ * _).sum pipe Math.sqrt

In this version, I love the infix zip, but I’m a little iffy about the infix pipe because I don’t really think of pipe as an infix operator. In my mind, every time I see pipe used over and over with the same function, that’s an indicator that this function should really be made available as a method. For example:

(a zip b).map(_ * _).sum.sqrt

Or even

(a zip b).map(_ * _).sum.(Math.sqrt)

As you can see, . and .pipe mean the same thing in this situation. Would it be possible to overload . to behave like .pipe in situations like this? Or would that overload . too much?

That seems like it would be really easy to mix up with the sugar for .apply

3 Likes

By coincidence, I read the PR thread on “implicit application syntax” for Dotty from 2017-18 or so, and someone did propose dot application. I wish I could find it amongst those long, long threads. I just wanted to point out that it came up at least once, in a certain context.

Edit: dot application

implicit val str: String = "x = "
def foo(x: Int).(y: String) = y+x
foo(0) // "x = 0"
foo(1).("Value: ") // "Value: 1"
1 Like

Thank you to everyone who responded so far. I feel like I need to come back to this discussion after learning more Scala!

If you’re interested, here’s the “opposite” discussion on the F# github about adding the fluent . methods to FSharp Core. Consider incorporating FSharp.Core.Fluent into FSharp.Core · Issue #1073 · fsharp/fslang-suggestions · GitHub. The majority of votes are against the proposal.

I love piping partial functions all day long, and I think I’ll find out soon enough how often I need to use .pipe to do so. But adding an operator, even a common one like |>, is a slippery slope. If Scala adds |>, it will probably open the floodgates for people asking for more symbolic operators for compose, andThen, etc. And it will certainly open the floodgates for people writing functions where the main argument comes last, not first. All of which detract from the Subject Verb Object essence of Scala? (Fresh eyes here–not sure if this is actually the essence of Scala :slight_smile: )

I think .pipe adds a little bit of friction and reminds library authors that super common functions should be exposed as methods for easier chaining.

1 Like