-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Initial pipeline rfc #2656
Conversation
886643c
to
f48ad01
Compare
One thought off the top of my head - rather than adding a new syntax for method calls in a pipeline ( 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:
EDIT: Updated with a better example of what I mean. |
Well, there is no difference between: let a = "a";
let b = a.to_string(); And the proposed syntax with 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
Both of these questions were mentioned in the RFC. I picked |
I hack on a language that uses 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 @17cupsofcoffee I don't find using FWIW, I think |
Yes, I also thought so. Just I elaborated on meaning of my
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 I always prefer pure functional style with pure functions, whenever possible.
These were also my thoughts. |
I see an unresolved problem: how compiler should treat the function invocation in every single case. /// 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? |
Point-free programming is related to De Bruijn indexes: 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 ( |
Taking your example:
Equivalent valid rust code could be:
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. |
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 (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.) |
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)]
])
} |
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. |
/// 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.
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
I also take your opinion and understand your concerns about having a language with wide features. But why did we move from Also, macros are not well-integrated into the language. Considering existing let num = pipe!(
4
=> (times(10))
=> {|i: u32| i * 2}
=> (times(4))
); Here you:
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
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.
Yes, this looks quite similar to mine. Your way just looks a bit more complicated to me, I don't personally like all these |
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.)
"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.
That argument reminds me of some of the discussion on the structural records RFC. I'll quote @graydon on this
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 |
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:
However, is it really better than this (nothing new, the trait approach is stated in the RFC)?
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. |
@vityafx
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. |
@jswrenn I agree wholeheartedly with @17cupsofcoffee here.
(Haskell uses
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
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?".
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.
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.
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. |
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. |
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. |
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.
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". |
@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. |
Hi @Centril,
How can I form one? By making a pull request to |
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. |
Sure! Just I don't know where and whom to list people who is interested (or where they can say they are interested themselves).
This would be really awesome. |
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. |
@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? |
I love both currying and the operators like 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 |
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. |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
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. |
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
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. |
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.