`Vec::append` but by value

Yeah, I'd assume that's what the compiler would do in one of the first steps (de-sugaring), especially since the expressions can get mixed/more complicated:

my_func(a.x()»y()?»z().build()?»w())
// de-sugared into (new-lines for readability)
my_func({
    let mut v = a;
    v.x();
    v.y()?;
    lut mut v = v.z()
    v.build()?;
    v.w()
})

It could even allow accessing individual fields, dropping all other unused fields:

my_func(a.x()»my_field.do_something())

Though one thing I have not thought of before is how to handle the ? operator and things like .unwrap(): Does the following two statements call A::z or <Result<_, _>>::z? The first would be a lot more useful but is inconsistent in how the first a.x()» works.

let a = A{};
my_func(a.x()»y()?»z().build()?»w())
my_func(a.x()»y().unwrap()»z().build()?»w())

Maybe the solution would be to start with », but that feels weird:

my_func(a»x()»y()?»z().build()?»w())

So basically: Does » mean call it on self of the previous call (which would for .unwrap() and ? be Result<_, _>) or on the first type in the expression (or something else)? The same goes for chaining async functions with .await:

// What is y called on in these cases?
my_func(a.x().await?»y())
my_func(a.x().await»y()?)
2 Likes

I have recently wanted a by-val version of PathBuf::push, and I regularly want this for the Command methods... so some general solution would indeed be quite nice. :slight_smile:

Shout-out to Dart's cascade syntax, it has great ergonomics imho. Unfortunately .. is already taken.

Another use case that I have quite frequently:

{
    let mut s = <vec expr>;
    s.sort();
    s
}

could be replaced by

<vec expr>»sort()

Unfortunately, that expression would evaluate to (). To address that, you'd end up where you started. Unless you could have a "dangling" ».

<vec expr>»sort()»

Can't say I'm a fan, though.

(@moderators, perhaps chaining can be pulled out into its own thread?)

Edit: Ah, thanks @CAD97 for clearing this up.

1 Like

You can use tap:

let collection = stream.collect::<Vec<_>>()
  .tap_mut(|v| v.sort());
4 Likes

It depends on how you define » to work.

The straightforward interpretation is for it to be a tap operation, i.e. $expr » $ident ( $args ) is an expression (left-associative in the same binding class as other $expr . $tts expressions) that effectively desugars to:

{
    let mut _tmp = $expr ;
    _tmp . $ident ( $args ) ;
    _tmp
}

which does .$ident on the receiver but then discards that result and evaluates to the receiver place. When $expr is a place expression instead of a value expression there isn't a single straightforward desugar because of how place autoref selection works and we want that to be able to select the binding mode as the most powerful required by the now multiple uses of the single place expression, but the simple by-move desugar works to illustrate if you imagine that the mut binding mode might instead be ref mut or ref, whichever is the weakest that will still typecheck.

You seem to have a different concept of how » would behave which is much more involved, and I'm not exactly sure what it is. The best I can tell, you would expect the first » in a method chain to behave identically to . (that is, f(v»sort()) is no different from f(v.sort())) but for each » after the first in a method chain to ignore the value its being used on and instead use the same receiver that the first » in the chain used.

Dart's cascade operator is somewhere between the two, in that further . manipulation within a cascade operates on that cascade step's result, but the overall cascade still evaluates to the receiver value and not the last cascade step like you're representing.

If Rust were to get a "cascade" operator, I hold that the straightforward tap/inspect semantic is the only one that really makes sense and properly composes with the rest of the language. It also directly aligns with using normal setters (fn set_prop(&mut self, prop: Prop)) as if they were builder-style "fluent" initializers (fn(self, Prop) -> Self or fn(&mut self, Prop) -> &mut Self)).

This does sacrifice the capability of Dart's cascade to further manipulate individual cascade steps' results (such as calling a method on a field; »field.method() would just be invalid), but it does so specifically to enable the final step in a chain to be a by-self method (e.g. .build()). I suspect the reason why you see » as having the more involved receiver selection behavior instead of more straightforward output substitution is in order to support cascading into such a finalization step. But we already have a way to invoke that step (.) so making » more complicated to enable it to move from its receiver -- which directly goes against the purpose of cascading -- isn't necessary.

4 Likes

Ironically, such a cascade operator would seem to then not work with fn(self, prop) -> Self style builder functions, as the function would consume the inherent temporary and drop the result, stopping the cascade then and there.

True, I think the best solution for that would be to put a hint into the resulting error message that you likely want to use . in that situation (since fn(self, prop -> Self could construct a completely new Self, so semantically » doesn't always make sense and doing that would just complicate things for little reason).

A similar hint could be added when trying to call v.a().b() when type(v) has a function b and a doesn't return Self or type(v).


Another way to think about it is how it would be used and what is influenced by the operator:

// Option 1: "Overwrite" the output of the LEFT expression
a»set_x()»set_y()»build() // Ambiguity in the return type of the entire expression
a.set_x()»set_y()»build() // Would be identical to the previous one
a»set_x()»set_y().build() // Would try to call build() on () => Compiler error

// Option 2: "Overwrites" the output of the RIGHT expressions (would be the effect of this desugar)
a»set_x()»set_y()»build() // Returns Self (so wouldn't make sense in a normal builder pattern
a.set_x()»set_y()»build() // Would try to call set_y on () => Compiler error
a»set_x()»set_y().build() // Returns return value of build() <-- correct use
  • Option 1 impacts the method immediately following it (right-arg) in terms of how it is called.
  • Option 2 impacts the expression result and thus anything called on it.

My initial mental model was option 1 (even though I thought about it like the desugar quoted above), so I think I caused some confusion/misunderstanding there). But by now I think option 2 makes more sense and is better suited (as it also matches how other operators work):

a»set_x()»set_y().build()

I would personally prefer with your Option 2 to keep the . precedence higher like dart to allow chaining within a field application and rather introduce another way to reduce the .'s precedence and return the final value instead of the original.

So, e.g., using ». as a strawman:

a»set_x()»set_y()».build()
// which would be equivalent to
(a»set_x()»set_y()).build()

Unless that's too subtle?

Kind of a side track but this is often covered for me by itertools' sorted function/method. In particular for Vec this won't be reallocating (afaict).

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.