Idea: universal pipelining (a.k.a. making @await generic)

The thing is, this dedicated pipeline also requires adding some form of currying functions to the language. Why, in this case, do you call f with one too few arguments? (It all depends on how UMCS goes, I guess, as that would also make “any” first argument the “self” argument.)

The problem with pipelining based on the first argument is that it privileges the first argument of free functions when they’ve never been before. E.g. Haskell functions and Rust methods are designed such that the “privileged” position (first in Rust, last in Haskell) are the “complicated” type that you’d build before and chain on, but free functions in Rust have no such idiom, which a non-closure based pipelining proposal immediately retroactively adds to every function.

Compare also "UMCS match" with closures instead:

z |> f(42) |> bar(v) |> |x| baz(9, x) |> spam;
z.match { x => f(x, 42) }.match { x => bar(x, v) }.match { x => baz(9, x) }.match { x => spam(x) };
z.pipe(|x| f(x, 42)).pipe(|x| bar(x, v)).pipe(|x| baz(9, x)).pipe(spam);

And my Pipe trait doesn’t need anything new in the language, even! It may be longer, but it’s clearer as it doesn’t add currying semantics where there are none. And consider a syntax that uses _ for explicit currying:

z |> f(42) |> bar(v) |> |x| baz(9, x) |> spam;
z |> «f(_, 42)» |> «bar(_, v)» |> «baz(9, _)» |> spam;
z.pipe(«f(_, 42)»).pipe(«bar(_, v)»).pipe(«baz(9, _)»).pipe(spam);
z.pipe(|x| f(x, 42)).pipe(|x| bar(x, v)).pipe(|x| baz(9, x)).pipe(spam);

(«» used to provide explicit scoping to the _-created curry, as that’s the biggest issue to making it work)

5 Likes

What if we had the keyword “tmp” do denote the value from the previous expression in pipeline…

foo(42)
    .bar(v)
    .baz(9, tmp)
    .match(tmp) {
        ...
    }
    .if tmp < 123 {0} else {1}
    .await;

Don’t take it too serious :slight_smile:

The point is that when I look at code like this…

foo() |> |x| baz(9, x)

… I realize that to me the whole point of pipelining is gone, because in fact what I read is…

let x = foo();
baz(9, x);

…but with the unusual |> |...| notation instead of the simple let.

1 Like

Hello, I’ve just submitted a syntax suggestion based on this discussion:

1 Like

if in postfix style could be advantageous as an alternative for unwrap_or_else when the latter needs to influence code flow, and is thus impossible to express as a closure:

some_computation().if let Some(result) { result } else {
    if self.environment.is_bad() {
        return early_return;
    }
    default_value
};

In the current syntax, this can only be ergonomically expressed if the return value implements Try and only then if the early return is actually an error value. This is not always true according to my experience especially for custom types. And even then, the code flow is not quite as obvious:

some_computation().or_else(|| {
    // This borrows all of `self` due to closure.
    if self.environment.is_bad() {
        Err(early_return)
    }
    Ok(default_value)
})?;
1 Like

I tried to look at how the if, match, and loop keywords would behave if they were to be chained. As far as I understand, these are the only keywords that are currently allowed in an expression context. A new await keyword would be the fourth such keyword:

I’ve just started a macro crate for exploring the general postfix operator ::():
https://crates.io/crates/sonic_spin

If the await syntax were a prefix keyword (similarly to box expr, or return expr ),
then this macro-crate would enable the alternative expr::(await) postfix syntax.

After writing the tests, I end up finding convenient to read (expr)::(postfix) syntax for loops and matches.

This is 100% subjective, but when being read they don’t attract too much attention and facilitate clustering of operations about postfix operators.
that is,
::() is not dense as @ (so it doesn’t attract much attention. eg. the line undershoot/overshoot doesn’t immediately follow the preceding meaningful character),
and :: is a lightweight separator which is easy to read and to cluster on (so you can easily find ::() if you’re thinking about it, and you also can comfortably ignore it if you’re thinking NOT about it).

1 Like

I’m very late to the async/await syntax discussion but came up with exactly the same idea. If future.await was chosen because it allows nice composition with the ? operator why not add a pipe operator first, which makes it very obvious how it should be written. I don’t know much about Elixir, but they surely got the pipe operator right:

https://elixirschool.com/en/lessons/basics/pipe-operator/

If Rust had this it would be very obvious how a postfix await should be written:

something |> await

The pipe operator just makes an awful lot of sense when using it with async/await:

42
    |> async_add(1) |> await?
    |> async_multiply(2) |> await?

In my opinion that reads a lot cleaner than the .await? syntax where you end up with code that doesn’t read from left to right but from inner bracket to outer bracket:

async_multiply(async_add(42, 1).await?, 2).await?

You could write both let response = await request() and request() |> await |> process_response(). Depending on the situation both syntaxes make a lot of sense.

tbh. I’m happy with any syntax as long as async/await lands in Rust in the foreseeable future. I just wanted to add my own two cents as future.await really looks alien to me and the pipe operator could be a great addition to Rust, anyways.

The problem is that Rust doesn’t have the universal currying and privileged last parameter like the functional languages with pipelining.

If we accept await? and ? as “pipelineable operators” (something I just made up), as well as their agglomeration into one “pipeline operation”, your example would necessarily at best look like

42
    |> |x| async_add(1, x) |> await?
    |> |x| async_multiply(2, x) |> await?

And on top of that, we don’t have a design and RFC for a pipeline operator. We have an accepted design and RFC for async/await, even if said RFC left syntax of await to be decided during stabilization.

If we blocked await on a generalized pipeline operation, it would be another epoch of time until we got stable access to async/await. That why the language team had the hard decision of picking the most advantageous syntax (lot the least objectionable “design by committee” choice, which would’ve been fake posing as a macro) that we could stabilize in a couple releases.

Pipelining is something that could potentially be made generally useful. It might live on ., it might live on |>. But that will be its own RFC, and async/await is now.

What would be the problem if X |> Y(P) was just syntactic sugar for Y(X, P)?

Real currying would be nice, too, but that’s something that doesn’t really fit in the language design of Rust, does it?

tbh. for the first version I could even think of X |> Y being limited to unary functions and special keywords like await. For the time being the compiler could just translate X |> Y internally into Y(X) and X |> await? becomes (await X)?.

I’m just curious if this option was ever discussed and if so why it was discarded. In my opinion this hits the sweet spot between a well known construct await X and still being able to write it in affix notation without overloading the . syntax.

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