Pipe results like Elixir?

I'm new to Rust, and have yet to finish the "book" so please forgive my ignorance if something like this exists already:

Does Rust have a pipe operator, or have any plans to implement a pipe operator like some other languages have (i.e. F# and Elixir)?

By pipe operator I mean this:

[ 1, 2, 3, 4, 5 ] |> iterate();

Where the results from the left hand side of the pipe ( |> ) are inserted into the first parameter of the function on the right hand side.

It makes code much easier to read especially in instances where you have something like this:

a( b( c( d() ) ) );

With a pipe operator you would write the above like:

d()
|> c()
|> b()
|> a()

or short hand: d() |> c() |> b() |> a()

This way the order of operations reads in like they happen rather than having to read the operations from the inside out.

2 Likes

You can do something similar with map chains on stuff like iterators, options, and results, e.g. d().map(c).map(b).map(a).

3 Likes

Yeah, it's kind of redundant when Rust has method syntax.

Method chaining isn't the same thing as piping though.

1 Like

Hi and welcome to Rust!

There is a crate (library) for that:

https://github.com/johannhof/pipeline.rs

The Rust programming language is extensible via macros, so you can add missing functionality to some extend. There is also a Haskell "do" like list comprehension:

https://github.com/goandylok/comp-rs

3 Likes

@willi_kappler, thanks. It's definitely a start. It would be nice if we could get something like this into Rust as a feature of the language so that we can pipe results without having to call a pipe macro.

For better or worse, that's very unlikely - Rust's philosophy is to keep the language as minimal as possible unless it's an essential primitive or there's only one way to do it, and to leave the rest to libraries. And especially in this case, although the feature makes a ton of sense in a functional language, it's a lot less useful in an imperative one like Rust.

3 Likes

I'm still not sure what the big difference is between method chaining and the limited version of piping that Elixir has. There are differences (Rust methods have to be specifically designated as such, and method calls do more inference than pipes), but are they really different enough to justify having both?

2 Likes

Method chaining does not inject a return variable from one method to another. It only calls a method on the returned object. Piping will take the return value from a function and inject it into the first (in most cases) parameter of the next function in line. @quadrupleslap is correct that is it primarily found in functional languages, but I hear a lot that people like to use Rust in a functional way.

That is how it works, though. The self might have special syntax, but what it actually is is the first parameter of the function. That's why this:

let sum = get_some_option()
  .unwrap()
  .sum();

can be rewritten like this:

let opt = get_some_option();
let it = Option::unwrap(opt);
let sum = Iterator::sum(it);
6 Likes

Where the results from the left hand side of the pipe ( |> ) are inserted into the first parameter of the function on the right hand side.

I think the primary difference here is that rust doesn't have partial functions.

3 Likes

I think the most idiomatic way to this would be to implement the functions you want to call through piping on the structs they will be called on (you can also implement traits on structs that are not defined in your own code, like i32 or Vec). This keeps your code organized better, and could actually save time in the long run if done instead of piping.

I don't know what you mean by "inject", but "to call a method on the returned object" means just "take the return value from a function and pass it into the first parameter of the next function in line".

For example, the following Elixir code, that I took from a tutorial:

"Elixir rocks" |> String.upcase |> String.split

is equivalent to the following Rust code:

"Elixir rocks".to_uppercase().split(' ').collect::<Vec<_>>()

In Rust the character used to split and the resulting collection type are made explicit. In addition, "collect" is required because "split" returns an iterator.

Could you provide an Elixir example that is not easily converted into Rust code?

3 Likes

I'm learning both Rust and Elixir, and I have the feeling that Rust's method syntax is closer to Elixir's pipe operator than to OOP's method calling. Traits seem similar to Elixir's Protocols.
One thing I don't like (for now) on method syntax is... like when you do this in elixir:

data
|> Ecto.Repo.get(params)  # returns struct
|> Map.get(:key)  # returns value

will I think be like this in Rust (if directly transcoded):

data
  .get()  // returns struct
  .get()  // returns value

is not much explicit. (Although we can't access struct values like this in Rust)
And you can't pipe like data.Foo::a().Bar::a() if there are multiple functions ( a ) with the same name for the Type (by different Traits.).

1 Like

can't you? I'm not certain, but I believe the Turbofish (::<>) should help there for giving type-hints, something along the lines of data.a::<Foo>().a::<Bar>() (Most famous in Iter::collect() to specify the desired output)

or maybe even plain old namespacing the function to the intended struct, effectively de-sugaring the method syntax (what the compiler does internally anyway):
Bar::a(Foo::a(data))

As for the original question why Rust doesn't have a |> pipe. I agree with quadrupleslap

The smaller Rust's core featureset, the quicker we can iterate and make the language better. We want to avoid making C's or Java's mistakes of taking too much into the core too quickly. E.g. Java's horribly broken Calendar implementation, that is still maintained (though deprecated) for backwards compatibility reasons...

Not saying that piping inherently is bad, just that Rust won't include it until the design has been optimised through several generations of library-API. And even then, only if it is the only sensible way. Why "lock it down" and freeze it in libstd when it can have so much more freedom to improve as a crate?
(I mean come on, even Future is a lib, and that's a built-in in practically every other language ever!)

1 Like
[ 1, 2, 3, 4, 5 ] |> iterate();

This would simply be

[1, 2, 3, 4, 5].iter();

And

d()
|> c()
|> b()
|> a()

Is simply:

d()
  .map(c)
  .map(b)
  .map(a)

Only restriction is that the map syntax above only applies if the function only accepts a single argument. Multiple inputs require using a closure to explicitly denote where the inputs go.

4 Likes

I'm definitely a fan of the forward pipe operator in other languages, so i experimented a little bit. I think in principle it would be sufficient to provide a trait with a .pipe method that applies self to a provided function that is implemented for all types. An operator might be neat, but to hash out the semantics this is fine.

Here is a link to a playground

pipe takes self by reference and applies it to the argument, pipe_move moves self and pipe_mut takes a mutable reference to self and a function that mutates its argument and returns the mutated left hand side. Since Rust is not purely functional, the last one could be useful.

What i've learned so far:

  1. Often it's necessary to wrap the function that is passed as argument into a closure, so that rust gets it's derefs right (for example vec_of_u8.pipe(from_utf8) does not work, but vec_of_u8.pipe(|x| from_utf8(x)) does).
  2. It goes a little bit against the structure of the std lib (and against other "idiomatic" apis). There are barely any free functions, so it's only really useful when one has to specify a method explicitly by trait namespace or with self defined free functions.

Nonetheless, i think pipes are useful, at least as a library. I often find it simpler and more straight forward to define some (internal) functionality as free functions and not as methods, especially if it's not clear if it's really belong to a single data structure.

2 Likes

Elixir's pipe operator spec states that the item on the left of the pipe ᐅ is called as the first parameter for the method immediately on the right. Most Rust methods that are implementations for anything typically have a reference to self as it's first parameter in it's method definition so in those cases those method calls will have an identical order as Elixir does. In Rust when you want to implement some additional behavior you create new traits (trait) and implementations of those traits (impl) each with a definition which may or may not include a reference to self.

So to create transformation methods like in Elixir you would need to do away with the idea of implementing traits and any reference to self and write your methods to take it's first parameter as the object on which you wish to work on. Then to process those arguments in the Elixir way (I believe) you would need a macro to convert d() ᐅ c() ᐅ b() ᐅ a() into a(b(c(d()))). This would work but the borrowing/cloning/type system would be very unconventional to implement in this way.

It can be done, but it feels like a lot of work to break away from Rust best practices to bring the familiarity of another language in. Not that that's a bad thing as you can see in the project Helix where they're creating macros that let you write Ruby like code in Rust. So I suppose it has its place.

Hi, I'm also new to Rust and used to the |> operator in Elm. In my mind, the main difference between piping and methods, is that methods only work if the creator of the function thought of the use case before and made it a method. Let's take serde_json::from_reader as a concrete example.

To read a json file, I'm currently doing:

let json_file_path = Path::new("path/to/file.json");
let json_file = File::open(json_file_path).expect("file not found");
let deserialized_data: SomeDataType =
    serde_json::from_reader(json_file).expect("error while reading json");

While with an "elmish" piping, I would do something in the like of:

let deserialized_data: SomeDataType =
    Path::new("path/to/file.json")
        |> File::open()
        |> expect("file not found")
        |> serde_json::from_reader()
        |> expect("error while reading json")

I get that it can be a matter of taste, but I hate naming temporary things just to avoid having nested parenthesis, that's why I like the piping syntax.

Any better way than my current rust version is welcomed! I'm still new to rust so struggling a bit with the way of writing things.

9 Likes