Idea: |> operator for postfix calls to macros and free functions

There have been a couple of proposals about postfix syntax recently: https://github.com/rust-lang/rfcs/pull/2442#issuecomment-391562545 for postfix macros and Idea: Simpler method-syntax private helpers for calling free functions with method syntax with limited scope.

The motivation for both is to allow reading expressions from left to right.

There is a fair amount of pushback on both of these proposal because they allow things that look like method calls that aren't method calls.

In both cases that particular objection could be overcome by introducing a different postfix syntax. Specifically, introducing an operator |> (bikeshed the actual symbol) such that expr |> free_fn(a,b, c) would desugar to free_fn(expr, a, b, c) and expr |> my_macro!(a, b, c) would desugar to my_macro!(expr, a, b, c). In the macro case, I'm not sure if it makes sense to evaluate the expression before passing it to the macro or not.

In reply to the post from @Centril:

So if I'm understanding you correctly, instead of . as the operator, something else? For example using |> as in F#: fun(receiver, args..) becomes receiver |> fun(args..) instead of receiver.fun(args..) and then for macros: receiver |> fun!(args..)?

Yes you understand correctly.

So I think such a solution costs more than reusing . syntax since . is already well understood -- "all" you need to do is adjust your brain's internal resolution engine. The advantage (and simultaneously the disadvantage) of reusing . is that it adds uniformity, now everything can be called with UFCS (Uniform Function Call Syntax) and everything can be called with (Uniform Method Call Syntax). As a bonus, we can also get disambiguation with paths in method call syntax.

Personally, I don't have a strong preference either way. But maybe having a different syntax has a better chance of getting accepted? The "|>" operator also has a potential advantage that it could potentially work for any free function or macro, whether the original author marked it for such use or not, which could also be an advantage (it works for legacy functions and macros) and disadvantage (it works for things that it doesn't make sense for).

So to clarify, my UMCS (Unified Method Call Syntax) proposal from the other thread is that when resolving recv.fun(args..) the compiler fails to find any inherent or trait impls (including with deref…) then it will try resolve it as a free function fun(recv, args..) instead.

The standard method syntax being:

  1. method – recv.fun(args..)

The following syntaxes are also added atop of the method syntax (and thus works for normal methods as well…):

  1. path – recv.path::to::fun(args..)
  2. trait – recv.<Type as Trait>(args..).

We then have the following legal call syntaxes:

// Function syntax:
fun(recv, args..)
Type::fun(recv, args..)
<Type as Trait>::fun(recv, args..) // UFCS

// Method syntax:
recv.fun(args..)
recv.Type::fun(args..)
recv.<Type as Trait>::fun(args..) // UMCS

All of inherent, trait, and free functions can now be called using function syntax and method syntax.

This change requires zero intervention in user code and will just start working for all existing Rust code.

We then have macros mac!(recv, args..) and recv.mac!(args..) per Josh’s proposal.

3 Likes

Bikeshed alert: if there was a special operator for macros (I’m not sure about that), I think it should contain !, as that one is associated with macros, while |> looks a bit alien (Invokes a shell-prompt association with me). So I’d prefer something like .! (if I write !( ), I call a macro, if I do .!, I method a macro ‒ and the ! is at the macro side). In other words, when choosing if the syntax should look similar to some language that already has such operator, or look similar to Rust constructs, I think the Rust constructs are better. But that’s personal preference.

As for UMCS ‒ it looks interesting and indeed unifying and I that sense I like it. It would make extention traits for one type obsolete. But I wonder what the effect on readability could be and if it could produce some surprises ‒ any way to find out?

And, all hail ().Ok() :innocent:

3 Likes

Counter-bikeshed:

|> is used in several functional languages (F# and Elixir) as a pipe forward operator, which does pretty much what the same thing as what this idea is trying to say,

// Piping a into the function, places it into the first argument for the function
a |> fn(b, c)

<=>

// Calling the function
fn(a, b, c)
1 Like

Am I wrong that this pipeforward operator could subsume both the functionality of (untyped) postfix macros and the unified method call syntax? This seems like a powerful, orthogonal feature. I strongly prefer to changing method resolution. I’d prefer -> to |> because it’s easier to type, but it might confuse C/C++ programmers. Perhaps .>, easy to type, still looks like a method, and has a visual cue of forwarding the lhs.

A question…

How would this be expected to handle evaluation order? If it’s a straight desugar, would it interact badly with the way that a method receiver is evaluated relative to the arguments? Does it have borrowing wonkiness? It would be a little weird to have a |> f(b, c) to evaluate a last if you’re calling it a ‘pipe forward’ operator. Especially once you start nesting things.

My general opinion on the whole matter is that I’d rather have a real currying mechanism instead, but to be honest, I haven’t thought it through much.

I think that it should be a simple syntactic sugar, which is expanded at the same time as macros, so a |> f(b, c) just expands to f(a, b, c), so the evaluation order is a then b then c, or whatever the Rust evaluation order is. If we decide to have it be an infix function, then it won’t make much sense without partial application like F#, but UMCS would probably be the better solution

1 Like

@JustAPerson

Am I wrong that this pipeforward operator could subsume both the functionality of (untyped) postfix macros and the unified method call syntax? This seems like a powerful, orthogonal feature. I strongly prefer to changing method resolution

Yes, exactly!

I’d prefer -> to |> because it’s easier to type, but it might confuse C/C++ programmers. Perhaps .>, easy to type, still looks like a method, and has a visual cue of forwarding the lhs.

I don't like "->" because that symbol has a completely unrelated use in function signatures.

@skysch

How would this be expected to handle evaluation order?

I think it would probably be best to desugar to something equivalent to:

let __temp__ = a;
f(__temp__, b, c)

so that it avoid weirdness with unspecified behavior for evaluation of arguments (if that is still unspecified, I don't know if it is), and prevents "garden path sentences" with macros.

My general opinion on the whole matter is that I’d rather have a real currying mechanism instead, but to be honest, I haven’t thought it through much.

that wouldn't work for macros

I don't think it is orthogonal; you can already use normal application, so there's nothing you can't already do; all that changes is the style really.

I'd use a match instead since it is the equivalent to let x = expr in x in functional languages and interacts better with temporaries:

match a {
    __temp__ => f(__temp__, b, c)
}
1 Like

That's true of both UMCS and postfix macros. However, postfix macros may be a special case of UMCS, but I strongly dislike further complicating implicit method resolution. This explicit syntax, very simple syntax enabling both features is great.

Wait is that actually different than just wrapping the let statement inside a new scope block?

Two thoughts:

  1. A feature like this could be really handy in conjunction with pipeline-structured data transformation libraries. An interesting model to look at is dplyr — I don’t think that could be exactly replicated in Rust, since it makes very heavy use of R’s lazy argument evaluation and arbitrary keyword arguments, but it should give you a whole class of use cases to think about, at least. (I regret to say I don’t know what similar things Rust might already have.)

  2. If we’re going to have this feature, we also need to have backwards assignment/let-binding. What do I mean by that? Well, if you’re crunching data with dplyr and you don’t know R has backwards assignment, you find yourself writing things like

    crunched <- (raw %>% select(...) %>% filter(...) %>% group_by (...)
                 %>% summarise(...))
    

    But it’s more readable to write it like this:

    raw %>% select(...) %>% filter(...)
        %>% group_by (...) %>% summarise(...))
        ->  crunched
    

    like how you would do it in shell with | and >. Translated to the language of this proposal, that would mean being able to write

    let raw |> select(...) |> filter(...) 
            |> group_by(...) |> summarise(...)
            |> crunched;
    

    and have that introduce crunched, not raw, as the new binding. If you hate that, that probably means you’re going to hate pipeline operators.

  3. Wouldn’t it be nice to be able to go the other way, too, sometimes?

    let crunched <| summarise(...) <| group_by(...)
                 <| filter(...) <| select(...) 
                 <| raw;

I doubt it would be exactly like that, because let wants to be able to parse a pattern after itself, and like that it was an expression there.

Though I suppose with the proposed is operator, you could do that as

raw
    |> select(...) 
    |> filter(...) 
    |> group_by(...) 
    |> summarise(...)
    is crunched;

using an irrefutable pattern to introduce a binding.

(Said without taking a position on whether this would be something I'd like.)

Well, strong dislike (why btw?) does not change the non-orthogonality.

The question with |> becomes: "Well, can I use it for normal methods as well?". If the question for that is "yes", then you've re-invented method syntax all over again with just two sigils instead of one. If no, then it is strictly less powerful and general.

See eddyb's comment here.

1 Like

I'm in this camp. Today, a method is either an inherent method or from one of the type's traits. Therefore the method can be considered to "belong" to the type. I think this mental model is very valuable when thinking about the code and it also works well for documentation. I like that your proposal keeps this mental model intact.

That'd be a good alternative. Here's how the code would look like:

let crunched = raw.>select(...)
                  .>filter(...) 
                  .>group_by(...)
                  .>summarise(...);

Is it better? I think that it combines better with normal method calls:

let x = raw.method1().method2()|>func().method3();

let x = raw.method1().method2().>func().method3();

@zackw I think we should stick with the simple let ident = ... syntax. A special let syntax is IMO not appropriate because it probably won't be used often enough to warrant it. Same goes for <|. Nice idea, but I think I wouldn't have a use for it if it existed. That's just my 0.02€. But I think it's good that you've mentioned it as a possibility.

From the two mentioned, I think .> fits better (I don’t know if I like the idea of another operator, but this one looks more „Rusty“). | evokes alternative/binary or association.

I just wonder, is introducing such operator backwards compatible? It was two tokens before, would be one now and some macro might have accepted the sequence before.

I doubt that it would break anything. However, it's possible to introduce it just as a part of Rust 2018: Rust 2018 operators thread

Another advantage of .> is also that it looks like as if it performs auto-deref. (Which I think it should perform)

Fuel for the syntax bikeshed: we already have two arrow operators in Rust, -> and =>, is it really wise to introduce yet another operator which looks like an arrow?

@HadrienG ^^’ I’ve searched a lot on my keyboard for alternative appropriate characters. It’s tough!

I think .> looks quite good. I can’t come up with anything better.

.> is ambiguous.

    fn foo(a: f32) -> f32 { a }

    let g = 2.>foo(1.);
    assert!(g);
4 Likes

@kennytm Indeed. It compiles… (playground link)