Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial pipeline rfc #2656

Closed
wants to merge 1 commit into from
Closed

Initial pipeline rfc #2656

wants to merge 1 commit into from

Conversation

iddm
Copy link

@iddm iddm commented Mar 5, 2019

Rendered

Hey guys, wanted to introduce my view on having pipeline (or pipes?) implemented in Rust language.

Open to discuss, open to close :)

P.S. I have seen some old RFC and it did not cover all the thoughts I put into my RFC.

@jonas-schievink jonas-schievink added T-lang Relevant to the language team, which will review and decide on the RFC. A-expressions Term language related proposals & ideas labels Mar 5, 2019
@iddm iddm force-pushed the pipelines-rfc branch 2 times, most recently from 886643c to f48ad01 Compare March 5, 2019 16:13
@Centril Centril added the A-syntax Syntax related proposals & ideas label Mar 5, 2019
@17cupsofcoffee
Copy link

17cupsofcoffee commented Mar 6, 2019

One thought off the top of my head - rather than adding a new syntax for method calls in a pipeline ("string" => _.to_string), it might make more sense to use a method reference ("string" => ToString::to_string), since that's already something you can do in Rust:

let initial_val = "string";

let method_syntax = initial_val.to_string();
let qualified_syntax = ToString::to_string(initial_val);

assert_eq!(method_syntax , qualified_syntax);

Also, a couple of thoughts on the syntax:

  • I'm not sure about using _ for the placeholder - everywhere else in Rust, a _ means 'this value doesn't matter' or 'this value isn't used', so seeing it being used as a parameter placeholder feels a bit strange to me.
  • Most other languages I've seen with this feature use |> for piping and =>/-> for lambdas - is there any reason for Rust to not use |> too?

EDIT: Updated with a better example of what I mean.

@iddm
Copy link
Author

iddm commented Mar 6, 2019

One thought off the top of my head - rather than adding a new syntax for method calls in a pipeline (_.to_string), it might make more sense to use a method reference (ToString::to_string), since that's already something you can do in Rust:

let a = "string".to_string();
let b = ToString::to_string("string");
assert_eq!(a, b);

Well, there is no difference between:

let a = "a";
let b = a.to_string();

And the proposed syntax with _ for intermediate value - in both these cases we already know the type we want to call the method on. Also, if you suddenly decide to change the trait name, you will need also to change it there. But anyway, this is not a big difference.

UPD: after your update, yes, it still not a big difference, we could simplify it so, but do we do this only because it is already implemented? Just _.to_string is a bit shorter and I find it a bit more convenient for an object with no other methods called to_string (it could come from many traits). We could also provide both ways: one if you need to specify the trait or just to be explicit, and one for simplicity and shortness.

Also, a couple of thoughts on the syntax:

* I'm not sure about using `_` for the placeholder - everywhere else in Rust, a `_` means 'this value doesn't matter' or 'this value isn't used', so seeing it being used as a parameter placeholder feels a bit strange to me.

* Most other languages I've seen with this feature use `|>` for piping and `=>`/`->` for lambdas - is there any reason for Rust to not use `|>` too?

Both of these questions were mentioned in the RFC. I picked _ as it is a placeholder for unused value, using that part that it is a placeholder just in another context (not for unused value, but for intermediate one). And => I picked because it is slightly easier to write then |> personally for me, also, because people are already familiar with it and it looks quite understandable in the code as it looks similar to an arrow while |> does not. Also, in clojure, -> and ->> are used for piping. These were my thoughts, I am open to discuss this, of course, I don't say that we must use exactly these tokens, we may change it if we find better ones.

@jswrenn
Copy link
Member

jswrenn commented Mar 6, 2019

I hack on a language that uses ^ for composition and _ for lambda shorthand (in exactly the manner proposed by this RFC). I'm a big fan of both of these features.

However, it seems to me like there are two distinct features here: a pipe operator and a lambda shorthand. Since both of these are going to be subject to endless bikeshedding, perhaps these should be separate RFCs? They're also independently useful. I often use placeholder syntax in map and filter calls; e.g.: iter.filter(_ > 5). I use pipelining less often, especially when method chaining is available.

@17cupsofcoffee I don't find using _ in the single-argument case that compelling, either. I think where it really shines is when you have something that takes multiple arguments and you want to fill in some, but not all, of the blanks (as in the filter example above).

FWIW, I think _ is actually a great choice of symbol for lambda shorthand. In both the name context (where it can already be used) and expression context, it refers to something that is filled in with a value, but cannot be referred to by name.

@iddm
Copy link
Author

iddm commented Mar 6, 2019

Since both of these are going to be subject to endless bikeshedding, perhaps these should be separate RFCs? They're also independently useful.

Yes, I also thought so. Just I elaborated on meaning of my _ in the end of writing the RFC :) It may be so that it could be better if there were written separate RFCs for the pipeline and the placeholder.

I use pipelining less often, especially when method chaining is available.

I'd also pick methods but then we have to do a lot of work with types and objects (I tried making something similar to std::iter::Iter trait myself) and it was not so quick as if I used functions. Also, if we call mutable methods of an object in a chain, the object shares state while pipelines are almost always stateless. This is just another view onto the solving of a problem. We may offer it for more general use :) Also, not everyone prefers objects in Rust, since Rust is multi-paradigm language, even though many things in the standard library (and crates) are object-like. As I have already said in the RFC, I do not tell people that we must write the code in this way, we can just provide this way, but the end user decides how and what he will use in his project.

I always prefer pure functional style with pure functions, whenever possible.

FWIW, I think _ is actually a great choice of symbol for lambda shorthand. In both the name context (where it can already be used) and expression context, it refers to something that is filled in with a value, but cannot be referred to by name.

These were also my thoughts.

@torkve
Copy link

torkve commented Mar 6, 2019

I see an unresolved problem: how compiler should treat the function invocation in every single case.
In OCaml and F# all the pipeline process is enabled by currying.
In Rust there is no currying and the partial arguments application leads to error.
So it's unclear to me from your proposal, what should be the expected results in Rust for each of the following statements, and how it should be implemented in language:

/// assume we have some functions
fn binary(arg1: u8, arg2: u8) -> u8 { ... }
fn binary_ho(arg1: u8, arg2: u8) -> dyn Fn(u8, u8) -> u8 { ... }

/// and now we call:
binary();
binary(1);
let x = binary(1);
binary(1, 2);
binary(1, 2) => ();
binary(1, 2) => binary();
binary(1, 2) => (binary(1, 2) => binary());
binary(1, 2) => binary(1, 2);
binary(1, 2) => binary_ho(1, 2);
1 => (binary(1, 2) => binary_ho(1, 2));
1 => (2 => binary(1, 2) => binary_ho(1, 2));

Where would be an error and where would be a result, and what result?
I still see only currying as a solution giving a consistent and meaningful behavior in all this examples, but there is not a word about it in the proposal, so may be I got your idea wrong.

@danielhenrymantilla
Copy link

Point-free programming is related to De Bruijn indexes:
I'd rather we use #0 instead of _ for the 'hole', so that when a function is defined itself as a pipeline there is no ambiguity as to which variable is referred to by _ :

let subber = |x, y| x - y;
40 => (20 => subber(_, _)); // ???

// Whereas
40 => (20 => subber(#1, #0)); // is not ambiguous, since
(20 => subber(#1, #0))
  = subber(#0, 20)
  = |x| subber(x, 20);

This shows that point-free programming, although an interesting topic to explore in Rust, should be dealt with in another RFC.

Pipelining is already a feature interesting enough on its own.

The biggest issue with all this is currying, which is far from trivial given Rust's ownership rules (move closures vs. non-move ones); I think we need existential -> impl Trait in return position in trait methods to fully enable currying in Rust.

@vitiral
Copy link

vitiral commented Mar 7, 2019

Taking your example:

let is_printed = 1 => adder(3) => multiplier(2) => _.to_string => printer(); // is_printed == true

Equivalent valid rust code could be:

let is_printed = {
  let a = 1;
  let a = adder(3, a);
  let a = multiplier(2, a);
  let a = ToString::to_string(a);
  let a = printer(a);
  a
};

This looks to me like something a macro could semi-trivially accomplish.

In my opinion, if this feature were desired then it could easily be accomplished by a crate. Therefore it should not be added to the core rust syntax. If such a crate became so massively popular and was such a huge ergonomics improvement that it was widely used, then maybe it could be added.

@ssokolow
Copy link

ssokolow commented Mar 7, 2019

My main problem with this RFC is that I get a strong C++ or Perl "More than one way to do it. Declare your preferred dialect in your CONTRIBUTING.md." feel from it and Rust takes a fair bit of inspiration from Python's "There should be one-- and preferably only one --obvious way to do it."

(Or, to put it another way, I just don't see the existing options as having enough shortcomings to justify adding another syntax for function/method composition.)

@I60R
Copy link

I60R commented Mar 7, 2019

You also might be interested in my pre-RFC and its discussion thread for similar syntax:

// Reusing your example:

fn perform_action(vendor_id: u64, model_id: u64, action: &str) -> Option<String> {
    Some(vendor_id
        .[find_vendor_id_in_database(this)?]
        .get_model_name(model_id)?
        .[do_action_with_model_name(this, action)]
        .[parse_action_result(this)]
    ])
}

@17cupsofcoffee
Copy link

17cupsofcoffee commented Mar 7, 2019

This looks to me like something a macro could semi-trivially accomplish.

There is prior art for implementing this as a macro: https://github.com/johannhof/pipeline.rs

It seems to work pretty well, with the main downside being that it's hard to create an ergonomic/natural looking syntax for pipelines within the confines of the macro system.

@iddm
Copy link
Author

iddm commented Mar 7, 2019

@torkve

I see an unresolved problem: how compiler should treat the function invocation in every single case.
In OCaml and F# all the pipeline process is enabled by currying.
In Rust there is no currying and the partial arguments application leads to error.
So it's unclear to me from your proposal, what should be the expected results in Rust for each of the following statements, and how it should be implemented in language:

/// assume we have some functions
fn binary(arg1: u8, arg2: u8) -> u8 { ... }
fn binary_ho(arg1: u8, arg2: u8) -> dyn Fn(u8, u8) -> u8 { ... }

/// and now we call:
binary();
binary(1);
let x = binary(1);
binary(1, 2);
binary(1, 2) => ();
binary(1, 2) => binary();
binary(1, 2) => (binary(1, 2) => binary());
binary(1, 2) => binary(1, 2);
binary(1, 2) => binary_ho(1, 2);
1 => (binary(1, 2) => binary_ho(1, 2));
1 => (2 => binary(1, 2) => binary_ho(1, 2));

Where would be an error and where would be a result, and what result?
I still see only currying as a solution giving a consistent and meaningful behavior in all this examples, but there is not a word about it in the proposal, so may be I got your idea wrong.

/// assume we have some functions
fn binary(arg1: u8, arg2: u8) -> u8 { 5u8 }
fn binary_ho(arg1: u8, arg2: u8) -> dyn Fn(u8, u8) -> u8 { ... }

fn main() {
    /// and now we call:
    binary(); // no arguments, usual rust error
    binary(1); // not enough arguments, usual rust error
    let x = binary(1); // not enough arguments, usual rust error
    binary(1, 2); // the returne value is ignored, no error

    // The line below is incorrect - there is nothing to call, for example, we can't
    // call a number, the same error as it would be for `let a = 5u8();`.
    binary(1, 2) => ();
    // not enough arguments, we passed `_` as arg1 into `binary`.
    binary(1, 2) => binary();
    // Multiple errors:
    // 1. Not enough arguments for passing in pipeline inside pipeline:
    //     `binary(1, 2)` evaluates to some value and we pass it to `binary`
    //     so there is no enough arguments again, as in previous example.
    // 2. After that, the first pipeline (outermost) is invalid: we can't
    //    pass a non-callable value to another non-callable value, like
    //    `5u8 => 6u8`, also `6u8` is non-callable, the syntax is incorrect.
    binary(1, 2) => (binary(1, 2) => binary());
    // Again, 2. from previous example.
    binary(1, 2) => binary(1, 2);
    // The result of this pipeline is incorrect: the pipeline assumes
    // one more argument in `binary_ho`, the third one, as it always
    // put intermediate value there implicitly as a last argument,
    // so it could be written explicitly like this:
    // `binary(1, 2) => binary_ho(1, 2, _)` which is incorrect.
    // If `binary_ho` had third argument, it returned a function which
    // we did not use.
    binary(1, 2) => binary_ho(1, 2);
    // Multiple errors:
    // 1. The same problem as above with `binary_ho` and not passing enough
    //    arguments to it.
    // 2. If 1. was solved by making `binary_ho` accept 3 parameters,
    //    it returned a function of two arguments and so we could not simply
    //    pass one argument with value of `1` (in the beginning) to it, as
    //    this returned function accepts two arguments.
    1 => (binary(1, 2) => binary_ho(1, 2));
    // Multiple errors:
    // 1. Incorrect invocation in `2 => binary(1, 2)`, as `binary` accepts 2 arguments,
    //    while we pass `2` to it as a third one.
    // 2. The same for `binary_ho`.
    // 3. The same for returned value of `binary_ho`.
    1 => (2 => binary(1, 2) => binary_ho(1, 2));
}

Please note, that I am still in doubt about having pipelines inside pipelines, I think it will reduce readability and understandability, but this is only my personal opinion.

@danielhenrymantilla

Point-free programming is related to De Bruijn indexes:
I'd rather we use #0 instead of _ for the 'hole', so that when a function is defined itself as a pipeline there is no ambiguity as to which variable is referred to by _ :

let subber = |x, y| x - y;
40 => (20 => subber(_, _)); // ???

// Whereas
40 => (20 => subber(#1, #0)); // is not ambiguous, since
(20 => subber(#1, #0))
  = subber(#0, 20)
  = |x| subber(x, 20);

This shows that point-free programming, although an interesting topic to explore in Rust, should be dealt with in another RFC.

Pipelining is already a feature interesting enough on its own.

The biggest issue with all this is currying, which is far from trivial given Rust's ownership rules (move closures vs. non-move ones); I think we need existential -> impl Trait in return position in trait methods to fully enable currying in Rust.

This is much more complex thing to have, and it is much more functional than rust is now, we also will have to make a lot of changes, it really requires another RFC. I also thought about this while making my RFC, but decided to give it another time. Also, seeing people not so acceptable about pipelines in the language now, I think this will never land.

About passing _ multiple times into one pipeline item, like subber(_, _), that is why I did not provide this example in the RFC: I think we should use Rust semantics of move by default and so use it here: if it can be moved (or copied), this is available, if there are two mutable references, this would be impossible. I was just thinking of it as a usual function call, I do not want to make Rust really another LISP or haskell, even though I love LISP. So I stick with existing rust rules about passing a value to a function by now.

@vitiral

Taking your example:

let is_printed = 1 => adder(3) => multiplier(2) => _.to_string => printer(); // is_printed == true

Equivalent valid rust code could be:

let is_printed = {
  let a = 1;
  let a = adder(3, a);
  let a = multiplier(2, a);
  let a = ToString::to_string(a);
  let a = printer(a);
  a
};

This looks to me like something a macro could semi-trivially accomplish.

In my opinion, if this feature were desired then it could easily be accomplished by a crate. Therefore it should not be added to the core rust syntax. If such a crate became so massively popular and was such a huge ergonomics improvement that it was widely used, then maybe it could be added.

I also take your opinion and understand your concerns about having a language with wide features. But why did we move from try! to ? then? Because of ergonomics, better integration with the language itself, because it is too common in Rust to unpack Result and Option types. In functional language it is also common to have pipelines (I even did it in C++) where you see your execution path step by step, as you deal with functions most (if not all) of time. As Rust is also multi-paradigm language, and considering its advantages of static checks (borrowing and others), it would be really nice to provide pipeline inside the language itself, easy integratable and requiring no more work like requiring and using a 3rd-party crate for this. Did we use try! as a separate crate?

Also, macros are not well-integrated into the language. Considering existing pipeline crate which I mentioned in the RFC, don't you find this not really ergonomic?

let num = pipe!(
  4
  => (times(10))
  => {|i: u32| i * 2}
  => (times(4))
);

Here you:

  1. Can't pass intermediate value in arbitrary position as I mentioned in the RFC, it is always the last one.

  2. You need braces all the time (what for, why?).

  3. You can't really understand what is [len] is called on, as it is not a usual syntax, it is not so understandable as in my RFC:

    let num = "abcd" => _.len;
    let num2 = "abcd" => str::len();

    Here you can clearly see that it is a method call. This is somewhat subjective of course, but I find this a little bit more clear.

  4. You need that pipe! all the time, or even pipe_res!, pipe_opt! and they can't be integrated into each other, you must use them separately, you can't use ? inside a pipeline, you are very limited using this, you need separate macros, you need braces, you need more efforts for this. My proposal simply eliminates all of this restrictions and provides a single, ergonomic way of making a pipeline. For example, this fails:

    #[macro_use]
    extern crate pipeline;
    
    fn times_opt(a: u32, b: u32) -> Option<u32> {
        Some(a * b)
    }
    
    fn main() {
        let length = pipe!(
            "abcd"
            => [len]
            => (as u32)
            => (times_opt(2)?)
            => [to_string]
        );
    
        println!("Values: {}, {}", num, length);
    }

    because of ? not being accepted by macro_rules!, so the author had to make two separate macros which accept only functions which return Option or Result.

@ssokolow

My main problem with this RFC is that I get a strong C++ or Perl "More than one way to do it. Declare your preferred dialect in your CONTRIBUTING.md." feel from it and Rust takes a fair bit of inspiration from Python's "There should be one-- and preferably only one --obvious way to do it."

There is no other way to do a pipeline using Rust and functions, we can only make chained method calls. As I have already told, rust is multi-paradigm language, but so it looks more object-oriented than functional actually. If we can do method chaining, why can't we do function chaining? I mean, in more or less normal way? There are also more than one ways of making loops over items in a collection, using iter or manual for, or for each (which is actually an iter), or even a while loop?

(Or, to put it another way, I just don't see the existing options as having enough shortcomings to justify adding another syntax for function/method composition.)

I did not provide a replacement. My RFC states, that if we do this, then there will also be another way of method chaining, as a side-effect. This is not my main goal - to duplicate existing functionality just because I want that. And again, we can't create a function chain but only method chain. This is what it all about, - function chaining.

@I60R

You also might be interested in my pre-RFC and its discussion thread for similar syntax:

// Reusing your example:

fn perform_action(vendor_id: u64, model_id: u64, action: &str) -> Option<String> {
    Some(vendor_id
        .[find_vendor_id_in_database(this)?]
        .get_model_name(model_id)?
        .[do_action_with_model_name(this, action)]
        .[parse_action_result(this)]
    ])
}

Yes, this looks quite similar to mine. Your way just looks a bit more complicated to me, I don't personally like all these {}, (), [] without a reason (so I like rust rule about unnecessary parenthesis inside if :)). I find your way a bit more difficult to write than mine but of course this is just my personal subjective opinion, but it seems we are pursuing almost the same (if not the same) goal.

@ssokolow
Copy link

ssokolow commented Mar 7, 2019

There is no other way to do a pipeline using Rust and functions, we can only make chained method calls.

It's having pipelines in Rust that hasn't been sufficiently justified for me. Chaining up non-methods like that looks quite alien and I have yet to see an example of code which is made so much better by having them that it convinces me of their worth.

(To be honest, in a language without currying, guaranteed tail call optimization, and at least a preference for recursion over iteration, they evoke an "Oh, no. Not another CoffeeScript" reaction from me.)

As I have already told, rust is multi-paradigm language, but so it looks more object-oriented than functional actually.

"X is a multi-paradigm language" isn't a license to pile on features, willy-nilly, to allow the language to fit every person's preferred way of writing code. That way lies C++.

For example, Python is also a multi-paradigm language, it's where that "There should be one-- and preferably only one --obvious way to do it." line I quoted comes from, and, unless they slipped into one of the recent 3.x releases that I've been neglecting to keep up on, it gets along just fine without pipelines.

If we can do method chaining, why can't we do function chaining? I mean, in more or less normal way? There are also more than one ways of making loops over items in a collection, using iter or manual for, or for each (which is actually an iter), or even a while loop?

That argument reminds me of some of the discussion on the structural records RFC.

I'll quote @graydon on this

The fact that there's an un-filled box in a particular table enumerating aspects of types does not at all warrant filling every such box in. Consistency should not be considered an design criterion that exceeds thrift.

You don't seem to be making exactly the same argument, but I feel like the same concerns and value for thrift apply.

One important element of deciding whether an RFC gets accepted is whether the benefit from adding it outweighs what it adds to the pile of things a newcomer must learn to become proficient.

(And I argue that chaining up unrelated functions the way pipelines do is a potential learnability/readability problem for people coming from languages not in the functional sphere.)

EDIT: Also, regarding your comment about try! becoming ?, that involved try! first proving itself as so ubiquitous and frequently used as a macro that it merited becoming ?. I haven't seen evidence of that that with macro-based pipelines.

@mickvangelderen
Copy link

mickvangelderen commented Mar 7, 2019

My main problem with this RFC is that I get a strong C++ or Perl "More than one way to do it. Declare your preferred dialect in your CONTRIBUTING.md." feel from it and Rust takes a fair bit of inspiration from Python's "There should be one-- and preferably only one --obvious way to do it."

(Or, to put it another way, I just don't see the existing options as having enough shortcomings to justify adding another syntax for function/method composition.)

I feel exactly the same way. Having multiple ways of achieving the same thing gives way to people having different styles and thus making it harder for different people to collaborate. You will be more proficient at reading and writing code in your preferred style. Obviously this is not a huge problem but, what does it actually give us? The given examples are fairly artificial but I would like to see examples of piping being applied pieces of code from (not using) popular crates, code that actually does something.

This example is given in the RFC to motivate it:

fn find_by_name(document: &select::Document) -> Option<&str> {
    Some(document.find(select::predicate::Class("name"))
         .next()?
         .text())
}

fn trimmed_and_owned(s: &str) -> Option<String> {
    s.trim().to_owned()
}

fn parse_name(html: &str) -> Option<String> {
    select::document::Document::from(html)
        => find_by_name()?
        => trimmed_and_owned()
}

However, is it really better than this (nothing new, the trait approach is stated in the RFC)?

trait DocumentExt {
    fn find_by_name(&self) -> Option<&str>;
}

impl DocumentExt for select::document::Document {
    fn find_by_name(&self) -> Option<&str> {
        self.find(select::predicate::Class("name")).next().map(select::Node::text)
    }
}

fn parse_name(html: &str) -> Option<String> {
    select::document::Document::from(html).find_by_name()?.trim().to_owned()
}

I expect that in most cases, we can get very far with what is already possible. I do not think that introducing more syntax into the language so that we can write code a little more concise will be an improvement. In fact I think it is hurtful because of the increased complexity in parsing (for both people and computers) and the therefore increased maintenance burden in rust and the entire ecosystem.

@torkve
Copy link

torkve commented Mar 7, 2019

@vityafx
From the explanation you gave I assume you are solving just one simple chaining case and you probably don't have enough understanding what underlying theory should be the ground for pipeline.
For example, currently I see the conflict between the explanation you gave for my example statements and your own question:

If we can do method chaining, why can't we do function chaining?

Basically method chaining has the underlying idea that every step of the chain is a valid value that has type and so on, so the whole chain can be considered step by step, it can be broken apart and any partial subject could be assigned to a variable. Your explanation above forbids this, so the chain must be parsed and evaluated only as a whole. Hence it gives us no further concepts, possibilities or features beyond the simple straightforward case considered, it is only a piece of syntactic sugar. So I must agree with @vitiral, this is a job for macros, not the language-level grammar construction.

@Centril
Copy link
Contributor

Centril commented Mar 7, 2019

I'm not sure about using _ for the placeholder - everywhere else in Rust, a _ means 'this value doesn't matter' or 'this value isn't used', so seeing it being used as a parameter placeholder feels a bit strange to me.

FWIW, I think _ is actually a great choice of symbol for lambda shorthand. In both the name context (where it can already be used) and expression context, it refers to something that is filled in with a value, but cannot be referred to by name.

@jswrenn I agree wholeheartedly with @17cupsofcoffee here. _ means "please infer" or more generally "I don't care" in Rust. This applies to two cases: type placeholders (which are inference type variables) and pattern matching _ => .... For consistency (and because I don't want to look for another syntax...), in expression contexts, _ should act exactly like it does in type contexts, that is, _ should stand for an inference value variable. For example, if you write let x: [u8; { _ }] = [1, 2]; then _ should infer to 2. This clashes with _ for lambdas and so I think lambdas will need to find a different syntax.

Most other languages I've seen with this feature use |> for piping and =>/-> for lambdas - is there any reason for Rust to not use |> too?

(Haskell uses $)

@danielhenrymantilla

The biggest issue with all this is currying, which is far from trivial given Rust's ownership rules (move closures vs. non-move ones); I think we need existential -> impl Trait in return position in trait methods to fully enable currying in Rust.

Currying / partial application is probably not that complicated. Save for one piece of information (the number of formal parameters a function has), it can be achieved in a syntax-directed manner. For example:

fn foo(x: u8, y: u8, u8) { ... }

let x = foo(1); // desugars to:
let x = { let _0 = 1; |_1, _2| foo(_0, _1, _2) };

let x = move foo(1); // desugars to:
let x = { let _0 = 1; move |_0, _1, _2| foo(_0, _1, _2) };

The compiler knows how many arguments foo has and so it knows how many extra parameters the lambda needs. This should also extend to methods as well.

@ssokolow

"X is a multi-paradigm language" isn't a license to pile on features, willy-nilly, to allow the language to fit every person's preferred way of writing code. That way lies C++.

I think everyone agrees that we shouldn't pile on features willy-nilly. I think the disagreement here is one of "is this willy-nilly?".

For example, Python is also a multi-paradigm language, it's where that "There should be one-- and preferably only one --obvious way to do it." line I quoted comes from, and, unless they slipped into one of the recent 3.x releases that I've been neglecting to keep up on, it gets along just fine without pipelines.

Python is good at not listening to it's own advice... ;) The language team is not philosophically opposed to having several ways to do things under certain conditions.

The fact that there's an un-filled box in a particular table enumerating aspects of types does not at all warrant filling every such box in. Consistency should not be considered an design criterion that exceeds thrift.

You don't seem to be making exactly the same argument, but I feel like the same concerns and value for thrift apply.

Irrespective of this RFC, I feel I must address Graydon's sentiment with respect to frugality over consistency. First, I think this is an unqualified blanket statement that is too unnuanced to serve as a good design guideline for us. I also disagree with the sentiment overall. If one is unwilling to let consistency (and composability in particular) exceed frugality, then I believe the likely outcome of the language design is a hard-to-learn and uncomposable patchwork. I think language design shines when things can be frugal, powerfully expressive, minimalistic, composable, consistent, and learnable all at the same time. That's of course easier said than done, but sometimes it is possible and it is a goal worthy of being our north star.

@mickvangelderen

In fact I think it is hurtful because of the increased complexity in parsing (for both people and computers) and the therefore increased maintenance burden and perhaps decreased compiler performance in rust and the entire ecosystem.

Decreased compiler performance is probably a non-issue as little time is spent in the parser. This proposal requires from what I can tell no backtracking so performance impacts should be negligible. The only noteworthy complexity is likely only for people.

@Centril
Copy link
Contributor

Centril commented Mar 7, 2019

I think this design space is interesting and I think there's something that should be done here. However, there are many alternatives to consider and the current proposal is likely not exactly the right fit for Rust. The RFC is also likely to be contentious and the language team does not have the bandwidth to give this proposal serious and sustained thought. Moreover, the proposal is unlikely to fit with our roadmap goals for 2019. Therefore, I propose that we:

@rfcbot postpone

this proposal.

Having said that, we may want to do something long-term. Thus, further investigation in a working group for ergonomics may be fruitful. For those interested you may consider forming such a group. This can include a balance of views including those who have expressed doubts in this RFC.

@rfcbot
Copy link
Collaborator

rfcbot commented Mar 7, 2019

Team member @Centril has proposed to postpone this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-postpone This RFC is in PFCP or FCP with a disposition to postpone it. labels Mar 7, 2019
@ssokolow
Copy link

ssokolow commented Mar 7, 2019

The language team is not philosophically opposed to having several ways to do things under certain conditions.

The line you were paraphrasing continues "as long as the technique is applied only to cases that are common or particularly painful."

My argument is that this hasn't been demonstrated to meet the "common or particularly painful" requirement sufficiently.

If one is unwilling to let consistency (and composability in particular) exceed frugality, then I believe the likely outcome of the language design is a hard-to-learn and uncomposable patchwork.

Fair enough... but I also don't see this as being one of those cases.

This feels very inconsistent with the design philosophy of the rest of the language to me.

That's why I qualified one of my previous comments with "in a language without currying, guaranteed tail call optimization, and at least a preference for recursion over iteration".

@Centril
Copy link
Contributor

Centril commented Mar 7, 2019

@ssokolow To clarify, my points were meant as general notes as opposed to directly tied to this RFC. :) IOW, I'm not saying one way or the other that this RFC as proposed fits "common" or "particularly painful" or that the benefits outweigh the costs. There are probably many different designs that try to satisfy the same needs here that have different cost/benefit calculus. A working group would likely be the right place to collect and discuss alternatives.

@iddm
Copy link
Author

iddm commented Mar 7, 2019

Hi @Centril,

Having said that, we may want to do something long-term. Thus, further investigation in a working group for ergonomics may be fruitful. For those interested you may consider forming such a group. This can include a balance of views including those who have expressed doubts in this RFC.

How can I form one? By making a pull request to lang-team repository?

@Centril
Copy link
Contributor

Centril commented Mar 7, 2019

How can I form one? By making a pull request to lang-team repository?

Well... uhmm... we are sorta in the midst of revamping our processes as a language team after the All Hands so this question doesn't exactly have a clear answer... so... cc @nikomatsakis :) I'd probably reach out to interested people first since a working-group needs more than one person. Ideally there would also be one language team member in such a group.

@iddm
Copy link
Author

iddm commented Mar 7, 2019

How can I form one? By making a pull request to lang-team repository?

Well... uhmm... we are sorta in the midst of revamping our processes as a language team after the all hands so this question doesn't exactly have a clear answer... so... cc @nikomatsakis :) I'd probably reach out to interested people first since a working-group needs more than one person.

Sure! Just I don't know where and whom to list people who is interested (or where they can say they are interested themselves).

Ideally there would also be one language team member in such a group.

This would be really awesome.

@Centril Centril self-assigned this Mar 7, 2019
@ghost
Copy link

ghost commented Mar 9, 2019

The attempt to create a new WG for the specific purpose of abrogating a ratified decision of another is an obvious attack on process and should be treated as such if the project is to survive its own increasing popularity.

The charter of an "ergonomics" WG could only possibly be to override the language group, or else to merely be an advisory body and thereby be a non-entity. As it's unlikely that well-meaning leadership would ever force a standing WG to be a non-entity, we can conclude that an "ergonomics" WG if created would have the power to override the language group. Hopefully the implicit decision to support WG creation can be reevaluated in that light.

@Ixrec
Copy link
Contributor

Ixrec commented Mar 9, 2019

@bele: I'm not aware of any pre-existing official decision from any Rust team or WG on the subject of a pipeline operator. What are you referring to?

@burdges
Copy link

burdges commented Mar 9, 2019

I love both currying and the operators like . and $ in Haskell. Yet, I'm weakly against adding them to Rust like this RFC proposes because they mostly just create noise without actual currying.

Instead, I'd prefer if someone developed an entire functional "overlay" language that compiles down to Rust, and interacts nicely with Rust's type, traits, etc., but uses a Haskell or ML like syntax for function arguments and evaluation, and solves currying using Rust's newer features, like ATCs. Doge lang attempted this for Python. I think such an overlay language would provide a much more ergonomic REPL and drive real improvements in Rust itself around iterators, generators, lenses, etc.


There are captured dysfunctional standards bodies like W3C in which "working group" means "the group blessed to make some decisions" @bele but actually the term has different meanings elsewhere.

I'd expect based on https://github.com/steveklabnik/rfcs/blob/2019-roadmap/text/0000-roadmap-2019.md#language and http://smallcultfollowing.com/babysteps/blog/2019/02/22/rust-lang-team-working-groups/#initial-set-of-active-working-groups that any such "soft" WG for Rust would largely act as a filter for the larger language team, with the de facto power to reject RFCs, but they'd only bring cases to the larger language team, not bless RFCs. I suppose "hard" WGs for things like unsafe code might often come up with more irrefutable answers, but that's fine.

That said, I believe "ergonomics" is a very poor name for a WG. Instead, I'd suggest a "Readability" WG with the express purpose of making Rust code more readable. Almost anything good that falls under "ergonomics" improves readability anyways.

In this light, there is no real improvement in readability in going from |x| foo(x,y) to foo(#0,y) while say improving #[derive(..)] helps enormously. I think the name "Readability" would help the WG spend it's time more wisely.

@cramertj
Copy link
Member

cramertj commented Mar 9, 2019

@bele

The charter of an "ergonomics" WG could only possibly be to override the language group, or else to merely be an advisory body and thereby be a non-entity. As it's unlikely that well-meaning leadership would ever force a standing WG to be a non-entity, we can conclude that an "ergonomics" WG if created would have the power to override the language group. Hopefully the implicit decision to support WG creation can be reevaluated in that light.

I disagree that creating working groups focused on assembling information, gathering consensus, and advising the language team on design issues are "non-entities." Ultimately the language team has the power to decide whether or not a feature is added, but working groups still have an extremely valuable role to play here.

@rfcbot rfcbot added the final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. label Mar 12, 2019
@rfcbot
Copy link
Collaborator

rfcbot commented Mar 12, 2019

🔔 This is now entering its final comment period, as per the review above. 🔔

@rfcbot rfcbot removed the proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. label Mar 12, 2019
@rfcbot
Copy link
Collaborator

rfcbot commented Mar 22, 2019

The final comment period, with a disposition to postpone, as per the review above, is now complete.

As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.

The RFC is now postponed.

@rfcbot rfcbot added finished-final-comment-period The final comment period is finished for this RFC. postponed RFCs that have been postponed and may be revisited at a later time. and removed final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. disposition-postpone This RFC is in PFCP or FCP with a disposition to postpone it. labels Mar 22, 2019
@rfcbot rfcbot closed this Mar 22, 2019
@marziply
Copy link

marziply commented Nov 5, 2022

I'm coming across this now as I've been toying with SeaQL which requires a lot of nested function calls to construct a query. Looking at some of their examples illustrates the sizeable chunks of nested calls you can create for even small and simple queries. Having played with it myself, I was reminded of the pipeline operator proposal for JavaScript, which has been mentioned in a similar RFC here.

I'm definitely not too fond of the => or -> approach so I would advocate for |>. I agree with what was said earlier in this conversation about piping:

If we can do method chaining, why can't we do function chaining?

In addition to this, I'm personally not too fond of the crazy lengths you can nest values via the new type pattern. Below is an extremely contrived example of what I'm trying to explain, in which ideally one could simplify the nested function calls with the pipe operator.

use std::sync::{Arc, Mutex}

struct Foo<A>(A);

struct Bar<B>(B);

struct Baz<C>(C);

// Each struct is given something like...
// impl X {
//   fn new() -> Self {
//     Self(...)
//   }
// }

fn main() {
  let value = Arc::new(Mutex::new(Foo::new(Bar::new(Baz::new(...)))));
}

Using this example, it would be arguably more readable using the pipe operator.

// ... snip ...

fn main() {
  let value = Baz::new
    |> Bar::new
    |> Foo::new
    |> Mutex::new
    |> Arc::new;
}

The examples above should be seen as the most basic form of what kind of code would gain readable value with the pipe operator. I would like to refer back to SeaQL as a better example, where it's very likely that you'll end up with a massive chunk of nested function calls. Obviously one could also argue that their implementation of a query builder is questionable, perhaps there's a more ergonomic way for them to do it. SeaQL is also just one example that I found literally this morning, so it's also possible that it's a poor example to begin with. The bottom line I'm trying to get to here is that personally, I would find it useful. I reckon I would use it enough to justify it as a language change. The JavaScript proposal is a feature I've been keenly waiting on for years now, although perhaps pipe operators simply can't translate to Rust.

It's been a few years since any activity was seen on this issue so I don't think there's much of a push for it, I just wanted to post my two cents on the matter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-expressions Term language related proposals & ideas A-syntax Syntax related proposals & ideas finished-final-comment-period The final comment period is finished for this RFC. postponed RFCs that have been postponed and may be revisited at a later time. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.