How often do you use the |> operator?

I really like pipe operator (|>) but if I see the source code of libraries I see that it is often used like this:

a = function_2(function_1(input_data)) |> gpu

rather than this:

a = input_data |>
        function_1 |>
        function_2 |>
        gpu

So I wonder how often julia programmers use the |> operator?
And why is it always used the first way instead of the second (which seems more readable)?
Does the second example constitute a bad practice?
Thank you, sorry for my english

10 Likes

In REPL, it’s very handy to interactively use one-argument functions with |> operator since it’s easier to type ... |> func instead of func(...).

In source code, I’m not a big fun of |>, but in the case you posted above, I guess the benefit is that one can quickly do some tests on CPU by commenting at |> instead of deleting brackets.

9 Likes

I used it quite a lot in R for dplyr, but never in Julia.

2 Likes

I sometime use it for do statements:
this

a = fun(x) do i
    ...
end |> wrapper

instead of

a = wrapper(fun(x) do i
    ...
end)

where wrapper is more often than not Ref

8 Likes

I also use it mostly interactively, almost always specifically as |> clipboard or |> plot.
I occasionally use it with broadcasting, values .|> callables, since callables.(values) doesn’t broadcast over callables. But it’s not a very common thing to do.

4 Likes

Never. I am fine with nested functions, or in source code

result1 = function1(input_data) # of course with functions and ...
result2 = function2(result2)    # ... variables given meaningful names
a = gpu(result2)

Better locations for stack traces, more understandable code.

14 Likes

I use it almost always (in REPL but mainly in source code), and nested functions are extremely rare. To allow that, I use heavily the Pipe and Chain modules.

I use classical syntax if there are at least 2 arguments with only one function:

JSON.print(file, data, 4)
result = expression |> JSON.parse

and I use nested functions sometimes to have some symmetry:

for (position, item) ∈ enumerate(items)

but I already surprised myself with:

for (position, item) ∈ items |> enumerate

For example, I really prefer this syntax over the classical one:

@pipe data |> filter(!isnothing, _) |> join(_, " ")

So, from the day I discovered this operator, its usage has grown continuously to become a typical trait of my own coding style… I must say that I don’t like parentheses and braces a lot, especially in streaks, even worse if there are trailing arguments!

It is also related to the way I read my code, and usually it comes from the start to the end (it is reversed with nested functions) so having the pipe operator allows me to follow the same way what happens with successive elements…

3 Likes

I use it a lot for chaining data transformations. Outside of that I spontaneously use it at the REPL.

2 Likes

I use it in the REPL as @yha. When writing code, I prefer to be more explicit as mentioned by @Tamas_Papp (easier to debug), or when suitable, use Chain.jl:

7 Likes

For chains of function calls, I often write two versions of the code: one with |> and one without. Then I keep the one that looks most readable. At a guess I’d say I keep |> 30% to 50% of the time.

I’ll probably use it more often if/when Julia finally supports the _ syntax for partial function application, since that would increase the number of cases where |> makes the code more readable. (There are packages like Pipe.jl and Chain.jl to help with that but the readability improvement doesn’t always outweigh the cost of an extra dependency.)

7 Likes

What I found using Chain.jl, was that it’s nice to have an almost normal-looking block of code, just that I’m spared from coming up with intermediate variable names. So I think that communicates quite well to a reader that you’re looking at a sequence of transformations where only the end result matters. I still don’t use it in packages, because the gain seems too low for another dependency, and also you’d have to decide a consistent style. You can’t switch all the time between normal function application and chaining or it will confuse people.

7 Likes

Personally I use the pipe operator almost all the time, especially in conjunction with Underscores.jl or Chain.jl.

When developing, I’l have a jupyter notebook cell with something like x |> f, tweak it until f does what I expect, then look at x |> f |> g, etc. This workflow also makes it easy to delete or comment out certain lines. I also really like reading left to right or top to bottom, as opposed to right to left with nested functions.

3 Likes

I believed, and the answers you received convinced me more, that the reason the |> operator is not widely used in Julia is mostly related to the fact that the base operator is pretty limited, and to have convenient implementations one has to use packages as Pipe.jl or Chain.jl, and at that time people don’t bother…

6 Likes

Do these chaining operations hurt performance?

No. Not unless the compiler completely gives up, in which case, you have bigger problems.

3 Likes

Do you have a reference supporting this claim? I remember chaining operators in R and Python slow down codes.

A lot of things hurt performance in R and Python because they don’t have optimizing compilers, or when you use a compiler, the language is too dynamic in the wrong ways to allow much optimization

julia> f(x) = x |> identity |> identity |> identity |> y -> y + 1
f (generic function with 1 method)

julia> code_llvm(f, (Int,); debuginfo=:none)
define i64 @julia_f_355(i64 signext %0) {
top:
  %1 = add i64 %0, 1
  ret i64 %1
}
julia> let x = Ref(1)
           @btime $x[] + 1
           @btime f($x[])
       end
           
  1.299 ns (0 allocations: 0 bytes)
  1.299 ns (0 allocations: 0 bytes)

Things like zero-runtime-cost abstractions are a core feature of julia. I guess I sometimes forget this even needs to be mentioned.

17 Likes

One thing I really like about Scala is that it has underscore syntax for anonymous functions, which makes piping or chaining much easier. Of course, in Julia you can do this with any one of several packages, but you still have to call the macro each time, which adds some friction.

Compare these two out of the box examples:

val z = (1 to 3)
  .map(_ + 1)
  .map(math.pow(_, 2))

println(z)
z = 1:3 .|> 
  (x -> x + 1) .|>
  (x -> x ^ 2)

Julia’s syntax wins on many fronts, but the slightly clunky anonymous functions really hurt function chaining/pipelining, especially when you have functions of multiple arguments.

3 Likes

It’s coincidental, but I use it quite a bit in Unitful: (u::Units)(q::Quantity) is defined such that 25u"kg" |> u"me" converts 25 kilogram into electron masses. But that’s more Unitful hijacking the notation.

10 Likes

I use it very rarely.
Basically only in the REPL because I have issues with my home/end keys.

I used to use it a fair bit, then I spent time around @ararslan.
Now I am convicted by their argument that it makes for harder reading in a lot of places as the function that controls the type of the return values is furthers from the assignment.

Though I do appreciate that it does work well for a pipeline like approach common to some workflows.
The distant function thing + just cognitive overhead of multiple ways to write the same thing means it isn’t worth it for me.
But it might be for others.

7 Likes