Ruby 2.7: The Pipeline Operator


Ruby 2.7 has added the pipeline operator ( |> ), but not in the way many Rubyists had expected.

The merge can be found here.

What it does

The pipeline operator is effectively an alias for dot-chaining, and from the source:

# This code equals to the next code. foo() |> bar 1, 2 |> display foo() .bar(1, 2) .display

...and from the test code:

def test_pipeline_operator assert_valid_syntax('x |> y') x = nil assert_equal("121", eval('x = 12 |> pow(2) |> to_s(11)')) assert_equal(12, x)
end

This, as it is written, is the current implementation of the pipeline operator as has been discussed in the Ruby bug tracker.

Chris Salzberg mentioned in the issue that the reason might have been its lower precedence than the dot operator:

The operator has lower precedence than ., so you can do this:

a .. b |> each do
end

With ., because of its higher precedence, you'd have to do use braces:

(a..b).each do
end

...in which he closes:

The bigger point IMHO though is that major controversial decisions are made based on this kind of very brief, mostly closed discussion.

I don't think that's a good thing for our community.

I am inclined to agree with him and several others discussing this change, and will lay out my reasoning for this below, including points in favor and counterpoints to the contrary.

Points in Favor

Of the points in favor, I have seen a few:

Ruby already has multiple syntaxes for blocks

One of the points raised is that Ruby already has multiple syntaxes for creating blocks, such as do ... end versus { ... }.

Pipelining is seen as a way to multi-line methods and make it clearer that that is being done:

# Original
foo.bar.baz # Current
foo .bar .baz # Alternate Current:
foo. bar. baz # Pipelined:
foo
|> bar
|> baz

Counterpoint

My objection to this would be that Ruby blocks are already confusing for newer developers, and are frequently a subject of contention when learning Ruby.

Adding a new language construct that does not have a clear differentiating factor will only exacerbate this and make the language harder to learn.

Precedence

Another point raised was that the pipeline operator has a lower precedence than the ., allowing for paren-free programming as mentioned above:

# Current
(a..b).each do
end # Pipelined
a .. b |> each do
end

Counterpoint

We also have precedence arguments over the english operators and and or, which are generally agreed upon to not be in common usage in the language because of the confusion they might cause.

As with the above issue of multiple syntaxes for the same task with different precedences, this will also confuse newer programmers.

Main Objections

With recent controversial features like Pattern Matching and Numbered Parameters there have been debates about the exact syntax, but the discussion had several come to the support of the feature as it added something distinctly new to the language.

In this case I do not believe this is so. The new pipeline operator feels like an alias of ., a syntactic sugar when it could have been substantially more for the language.

The main points in favor relate to a difference in precedence evaluation, an issue that has caused great confusion in newer Ruby programmers in the past, and still continues to this day.

Introducing a new symbol in a language should add a new and more expressive way to do things in that language. I do not believe this achieves that goal.

What Could It Have Been?

The main points to the contrary are that the pipeline in other languages is a powerful and expressive feature for code. I would like to show you some of those implementations and let you judge for yourself their merits.

In Javascript

TC39 has discussed a pipeline operator which is currently under careful evaluation. As Javascript is a very similar language to Ruby, many ideas have been shared between the two languages, and many more are very possible.

In their example:

const double = (n) => n * 2;
const increment = (n) => n + 1; // without pipeline operator
double(increment(double(double(5)))); // 42 // with pipeline operator
5 |> double |> double |> increment |> double; // 42

The pipeline operator is used to provide the output of the left side to the input of the right side. It explicitly requires that this be the sole argument to the function, of which can be circumvented with currying techniques in both languages to partially apply functions.

Javascript Pipeline Applied to Ruby

This is very similar to the Ruby convention of then:

double = -> n { n * 2 }
increment = -> n { n + 1 } double[increment[double[double[5]]]]
# => 42 5.then(&double).then(&double).then(&increment).then(&double)

If the pipeline operator were to be an alias of then that can reasonably infer to_proc / & and method calls, it would look quite the same as Javascript:

5 |> double |> double |> increment |> double
# => 42

This achieves a few major items:

  1. It removes the need for explicit block-tagging with &
  2. It simplifies the then variant of the code
  3. It (ideally) can intelligently deal with both methods and procs

By dealing with methods and procs, I mean that this would not change the output of the above code:

def double(n) n * 2
end increment = -> n { n + 1 } 5 |> double |> double |> increment |> double
# => 42

This, I believe, is a more true-to-ruby implementation that is expressive and elegant, allowing for simpler syntax to carry a more complicated idea seamlessly.

It uses the idea of duck-typing with operators to say that these two should behave the same to achieve a syntax which is very powerful.

In OCaml and F

The example from OCaml and F# look very similar, so we'll focus on the F# variant which also highlights the difference between this and composition (which I won't cover here, but worth a read):

It works by piping the last parameter into the function:

let doSomething x y z = x+y+z
doSomething 1 2 3 // all parameters after function
3 |> doSomething 1 2 // last parameter piped in

I am not immediately familiar with F#, but this appears to be a currying implementation which achieves some of what was mentioned in the Javascript example

F# Pipeline Applied to Ruby

This introduces an interesting idea of currying, which is applying arguments to a function and waiting for a final argument to call through to get a value. We already have this in Ruby with curry:

adds = -> a, b { a + b }.curry adds[2, 3]
# => 5 adds[2]
# => #<Proc:0x00007fe8ab6996a0 (lambda)>
adds[2][3]
# => 5

While I don't think auto-currying would match Ruby well, it would make an interesting addition to the pipeline operator:

adds = -> a, b { a + b }.curry def double(n) n * 2
end increment = -> n { n + 1 } 5 |> adds[2] |> double |> increment
# => 15

In Elixir

Elixir works much the same way as the Javascript implementation, except in that it can also "soft-curry" functions that are waiting for an additional input if they're in a pipeline.

Again, I'm not familiar with Elixir to a deep level, and would welcome corrections on this:

"Elixir rocks" |> String.upcase() |> String.split()
["ELIXIR", "ROCKS"] "elixir" |> String.ends_with?("ixir")
true

Interestingly it contends with one of the issues Ruby would have with some of this, being ambiguous syntax around parentheses:

iex> "elixir" |> String.ends_with? "ixir"
warning: parentheses are required when piping into a function call.
For example: foo 1 |> bar 2 |> baz 3 is ambiguous and should be written as foo(1) |> bar(2) |> baz(3) true

Combining Worlds

Now here's a shocking revelation: I like the idea of using it as an alias for ., but....

...it should be a combination of both that and the above ideas from other languages:

5
|> double
|> increment
|> to_s(2)
|> reverse
|> to_i

This gives us a substantial increase in expressive power while unifying the current implementation with established ideas from other languages. I would be very excited if such an implementation were to come into use as it would combine the best of the functional world with Ruby's natural Object Orientation to achieve something entirely new.

To me, that's what Ruby is, achieving something new with ideas from around the world and from different languages. We bring together the novel and exciting and make it our own, and I believe with the pipeline operator we have an amazing chance to do this!

I just do not believe that the current implementation fully realizes this potential, and that makes me sad.