Proposal: forward pipe operators #74
Replies: 78 comments 14 replies
-
Currently |
Beta Was this translation helpful? Give feedback.
-
@alrz Requiring parentheses would violate the principle of least surprise, IMO: SomeType e = default(SomeType);
Action<SomeType> F(int x) => blah;
void F(int x, SomeType st) => bleh;
e |> F(1); //calls the second overload
e |> F(1)(); //calls the first overload Also, currying is usually done from the left, not from the right: static Func<T2, TResult> Curry<T1, T2, TResult>(Func<T1, T2, TResult> func, T1 t1)
=> t2 => func(t1, t2); Inserting a parameter in front of the supplied parameters feels weird, I'd rather support something like dotnet/roslyn#3561 or dotnet/roslyn#8670 or dotnet/roslyn#3171 |
Beta Was this translation helpful? Give feedback.
-
While I disagree that it would be any useful to forward to return value of the RHS in context of C#, I think it would help to provide language grammar you have in mind for the forward pipe operator. |
Beta Was this translation helpful? Give feedback.
-
I'm noticing a problem with proposals created by someone other that the team. In the latter case, there is a proposal spec that is linked to that shows the details, such as syntax. Would it be worth creating a gist for example, that could used for the spec for this proposal (same idea would apply to other community proposals too)? |
Beta Was this translation helpful? Give feedback.
-
@DavidArno I guess I could fork the repo and push the proposals there. |
Beta Was this translation helpful? Give feedback.
-
Ah good point. Hadn't thought of doing that. Excellent idea as a PR from that is easier too. |
Beta Was this translation helpful? Give feedback.
-
@alrz Could you add some markdown examples of grammar specs to your proposed issue template? This would help a whole lot. |
Beta Was this translation helpful? Give feedback.
-
In that proposal you could use a general (or restricted, depending on the precedence) expression as RHS since there is no syntactic requirements. Then allowed forms will be checked semantically. e.g only permit expressions that a value can be forwarded to, namely, invocation-expression, object-creation-expression, an await-expression that contains any of applicable expressions (recursively), etc. Note that |
Beta Was this translation helpful? Give feedback.
-
@alrz in my case it's not restricted at all, since the pipe expression replaces all invocation-likes: the rhs provides an invocable. I think only object creation expression is weird, since it doesn't actually provide an invocable and has to be syntactically valid without both argument list and initializers now. I should check if my approach works with await, though. |
Beta Was this translation helpful? Give feedback.
-
It would be nice if piping to arg-positions other than the last will be supported. This can be achieved by a new place-holder syntax, e.g.:
Alternative place-holder suggestion:
This is especially useful given that methods in C# codebases typically aren't designed for piping purposes. |
Beta Was this translation helpful? Give feedback.
-
@YaakovDavis yes, shorthand lambdas are an option as well, but they have very complicated boundary detection rules. Maybe if the placeholders were restricted to argument lists and not arbitrary expressions... but then they wouldn't be as useful as full-powered shorthand lambdas. |
Beta Was this translation helpful? Give feedback.
-
Are you referring to the proposed lambda syntax from dotnet/roslyn#5445? |
Beta Was this translation helpful? Give feedback.
-
@YaakovDavis I think you must've linked the wrong proposal. Anyway, yes, I am referring to that syntax. I wouldn't call you proposal orthogonal to the shorthand lambda, since piping to a shorthand lambda would look the same as piping to an invocation with a placeholder: 2 |> M(1, .., 3);
2 |> M(1, @, 3); This makes placeholders a strict subset of the shorthand lambda functionality. Return values are commonly used as method receivers, not just regular arguments, so if we allow placeholders to be used there as well, they might be much more useful than I originally thought... |
Beta Was this translation helpful? Give feedback.
-
@orthoxerox Seems like you're referring dotnet/roslyn#3561. Am I correct? Let's continue the discussion after confirming that we're on the same page :) |
Beta Was this translation helpful? Give feedback.
-
I'm not sure why forward pipe operator makes a language more "functional". More "fluent", maybe. In my opinion exposing any of these four operators opens a can of worms as you'll soon run into paradigmatic differences between C# and functional languages. Currying and overloading are two big ones. Those two features generally can't coexist in the same language (or at least in the same context, which is the line that F# walks). In F#, piping an expression into a function that accepts multiple arguments results in a currying operation. That clearly can't happen here. So you still end up with having arguments having to be defined and embedded at the call site. Personally I prefer extension methods. They achieve roughly the same functionality but fit more into the C# syntax. Hopefully local extension methods will be supported at some point. |
Beta Was this translation helpful? Give feedback.
-
The ECMAScript proposal made |
Beta Was this translation helpful? Give feedback.
-
One thing I'd love to use this for is for "postfix-casts". It feels unnatural to me that the C# cast Compare:
With:
I feel |
Beta Was this translation helpful? Give feedback.
-
I do fun programming again with
|
Beta Was this translation helpful? Give feedback.
-
Agreed. For me the main benefit is the postfix notation which naturally fits to my train of thoughts / transformation chain from input to required result. This chain naturally flows forward - it is where the piping or your extension methods fit. Given the usual notation I often need to return back to store the result of first transformation, then jump forward again to use the result. It may not need to be applied everywhere, but it helps me a lot in small methods or LINQ queries, to produce a cleaner code. Usually it enables to convert such methods from statement to exptession-body, cutting more noise. Even if it is only may be applied to single parameter methods, that's fine. One improvement at time!
Extension methods:
Pipe operator(s):
|
Beta Was this translation helpful? Give feedback.
-
Instead of tying placeholders and the pipeline operator together (at cost of increased complexity to both), can we instead have two simpler operators? v |> f |> g |> h
// => always deugars to (and typechecks/dispatches identically to)
h(g(f(v)))
// without regard to number of arguments or overloads of f, g, h In order to deal with multi-argument functions you can explicitly do: v |> f |> (x => g(42, "foo", x)) |> ... If the simple lambda wrapping f(?, "foo")
// => desugars to
x => f(x, "foo") Now you can use the two features in isolation or together: v |> f |> g(42, "foo", ?) |> ... Of course special casing the emit of |
Beta Was this translation helpful? Give feedback.
-
The problem with resorting to lambdas is that they always allocate - and they often capture. This is going to create a lot of heap debris:
Whereas, with the original proposal, this would not:
|
Beta Was this translation helpful? Give feedback.
-
I guess it's too idealistic to hope there's some emit or JIT optimization that makes |
Beta Was this translation helpful? Give feedback.
-
AFAIK, there isn't at present - and I believe designing a language feature in a way that it requires CLR changes dramatically changes the likelihood of implementation. |
Beta Was this translation helpful? Give feedback.
-
@masaeedu @theunrepentantgeek that doesn't have to be a CLR change. |
Beta Was this translation helpful? Give feedback.
-
If you've written code like |
Beta Was this translation helpful? Give feedback.
-
No, that's writing code like |
Beta Was this translation helpful? Give feedback.
-
The |
Beta Was this translation helpful? Give feedback.
-
Is allocation on every invocation, or is a single capture-container object created on-demand and shared by subsequent calls (a-la ...and wouldn't the JIT inline the method invocations anyway? |
Beta Was this translation helpful? Give feedback.
-
I believe that it depends on whether the lambda involves any captures. If it doesn't capture, I believe it will be allocated once. But if it does capture, I'm pretty sure that lambdas are separately allocated for every execution. |
Beta Was this translation helpful? Give feedback.
-
I would warn against writing code in this manner. Often, you want to be able to see the intermediate results of the methods. Debugging code line by line is made difficult by these complex constructs. |
Beta Was this translation helpful? Give feedback.
-
Forward pipe operators
Summary
Operators for passing values to methods/delegates/constructors/arbitrary expressions.
See also dotnet/roslyn#5445, dotnet/roslyn#3171
Motivation
Promoting the use of static (preferably stateless) functions.
Detailed design
Grammar
Functionality
Binding
2.1. The rhs is visited by a placeholder finder
2.1.1. Pipe expressions do not check their rhs for placeholders
2.1.2. Placeholders report they are a placeholder
2.1.3. Other nodes recur
2.2. If placeholders are present, the rhs is bound as is using PipeBinder
2.2.1. Placeholders are bound as BoundLocals by PipeBinder (regular binder binds them as BoundBadExpressions)
2.3. If placeholders are not present, the rhs is bound as an invocation:
2.3.1. If the rhs is an ObjectCreationExpressionSyntax with arguments, it is bound as a BoundBadExpression (tbd)
2.3.2. If the rhs is an ObjectCreationExpressionSyntax without arguments, it is bound as a BoundObjectCreationExpression with a single argument
2.3.3. If the rhs is a ConditionalAccessExpressionSyntax, it is (TODO: explain the logic in the prototyp)
2.3.4. If the rhs is an AwaitExpressionSyntax, (TODO: think about that)
2.3.5. Otherwise the rhs is bound as an invocation expression with one argument
Code Gen
2.1. If the BoundPipeExpression is conditional:
2.1.1. If the result is
null
or aNullable<T>
with no value, a default value of the bound rhs type is emitted2.1.2. Otherwise the rhs is emitted
2.2. If the BoundPipeExpression is not conditional, the rhs is emitted
Drawbacks
I don't really see any.
Alternatives
None.
Unresolved questions
What is the correct precedence of the pipe operators? In my (invalid) prototype I placed them between ternary and coalescing operators, but I can't remember why.
Are there any other syntax nodes that protect placeholders inside? Lambdas/delegates?
Should we have an operator that chains function calls using
SelectMany
?Design meetings
None that I know of.
Beta Was this translation helpful? Give feedback.
All reactions