Replies: 35 comments 33 replies
-
Is this issue acceptable for pull request ? |
Beta Was this translation helpful? Give feedback.
-
What's the difference between this and #74 ? |
Beta Was this translation helpful? Give feedback.
-
@a1n1 Not sure, but supposedly, there will be a "champion" issue once this passes the first stage. @chrisaut This has the input parameter added to the argument list. That proposal requires there be no argument list i.e. the LHS would be forwarded to the "value" of the RHS, hence, it would not permit forwarding to methods with more than a single (formal) parameter and you'd need to use |
Beta Was this translation helpful? Give feedback.
-
I don't understand what actual problem this solves. Sure, it unwraps the call chain so it places the methods in their actual execution order, but it doesn't make the code any shorter. There are few (if any?) cases I've encountered where I couldn't follow nested parameters. You could turn the example into:
And it's just as easy to read, as the parenthesis make the execution order clear. It's also crystal clear, within which parameter an argument will actually be placed, without worrying about implicit conversions coercing the actual parameter you're specifying. What implications does this have for IntelliSense, would it provide context on what parameter it goes into by hovering over the |> characters? Wouldn't you also have to ensure the right-hand side of the operator, within the intellisense that it's accepting one forwarded operator in some manner? The only thing I do like is the argument-level short-circuiting, though that sounds like it could be a feature request of its own for standard calling conventions. |
Beta Was this translation helpful? Give feedback.
-
that's not the intention here.
It's not as easy to write since you should be writing the methods in the reverse order. Using locals however, can help with that but then you have to leave the expression context.
What syntax are you imagining for that as a separate feature? |
Beta Was this translation helpful? Give feedback.
-
@alrz : The biggest gripe I have with the feature is the disconnect between what you're writing and where the data actually goes. The notion that it would bind differently based on the type of the parameter: overload resolution is already complicated enough as it is, if there was some way to explicitly stipulate which parameter was the target of the forwarded pipe, through say a special character combination of some sort:
This way if it were targeting a specific parameter, the notion of overload resolution wouldn't be a concern, if you wanted parameter That would make the binding of the operation much simpler to understand, and I think the reader would benefit, as well.
Probably something like: ?? prefixing the argument As for weaving it into the language spec as it is today, it would probably be something like:
It wouldn't be allowed on the out parameter because it's implicitly unassigned within the scope of the called method, so it wouldn't really make sense. The default value of the method would be the result of the expression if it short-circuited. If you were to take the two ideas together, you would only need one pipe-forward operator:
You'd end up with:
|
Beta Was this translation helpful? Give feedback.
-
Here I suggested to just "add the LHS as an argument to the method invocation on the right." (other than some cases with optional arguments which can be excluded if seen as rather unreliable than helpful). that doesn't require you to fallback to lambda when there is more than a single parameter (which I've found very likely). There's a discussion on the other variation here: #74 |
Beta Was this translation helpful? Give feedback.
-
@alrz I read further on the other discussion and saw it talks about this suggestion so deleted my post. Seems you got in there right before, sorry about that :) Since it's too late and I've been replied to now, I guess I'll sum up what you were replying to: This doesn't look to me like it fits the established expectations and design of the C# language. The basic pipe operator part of #74 fits closer to what people would closer to what would be intuitive in C# with delegation, whilst this RHS proposal currently requires a mental context-switch from "normal overload resolution" to "pipe papp", where how you would by-default mentally parse the syntax is incorrect. This...seems problematic. If a form of currying gets added in the future it would behave similar to the above does, and an intermediate lambda solves so much of the issue in the short-term that it is what I'd expect that for an initial implementation into C#, leaving room for currying/papp later without committing to any particular syntax. (Interestingly, I've seen the usage of an intermediate lambda cited as a reason for a lack of motivation in introducing currying to ECMAScript nowadays as well). |
Beta Was this translation helpful? Give feedback.
-
@alrz: It would be cool to be able to pipe tuples as arguments, but the argument order could be a problem.
|
Beta Was this translation helpful? Give feedback.
-
Can we have pipe aliases? For example:
Now I would have ReadBytesAndComputeHash method defined in the class that I can reuse it elsewhere. The advantage of this is that I achieve 2 goals at the same time:
Otherwise it would be impossible to see the entire pipeline with all steps in one place, but at the same time be able to reuse parts of it. The same could apply for linq:
|
Beta Was this translation helpful? Give feedback.
-
Sorry if I missed it in the discussion, but why is it: Console.ReadLine()
|> File.ReadAllBytes()
|> SHA1.Create().ComputeHash()
// ... instead of the following? Console.ReadLine()
|> File.ReadAllBytes
|> SHA1.Create().ComputeHash
// ... Isn't the function here |
Beta Was this translation helpful? Give feedback.
-
I think it's because simply naming a method (as Including the |
Beta Was this translation helpful? Give feedback.
-
Isn't the overload determined by the LHS of the expression? |
Beta Was this translation helpful? Give feedback.
-
It's unambiguous for But, what happens if that changes in the future? For example, and purely for illustration, what if a future version of the framework adds a new overload: The method group would then return two methods, which isn't unique. If the parenthesis were not permitted for the single parameter case, then we'd end up with a disconnect. For the original overload:
But for the new overload:
I think it's substantially cleaner to require the Also, and quite separate from my argument above, I suspect that leaving out the |
Beta Was this translation helpful? Give feedback.
-
To my mind Console.ReadLine() |> File.ReadAllBytes always just means: File.ReadAllBytes(Console.ReadLine())
File.ReadAllBytes(new UTF7Encoding())(Console.ReadLine()) If we want to deal with a two argument function, we can just do: Console.ReadLine()
|> (x => File.ReadAllBytes(x, new UTF7Encoding())) If that's too painful, it can be mitigated at the library level, or with an independent language feature for making partial application more convenient, e.g: File.ReadAllBytes(?, new UTF7Encoding())
// desugars to
x => File.ReadAllBytes(x, new UTF7Encoding()) |
Beta Was this translation helpful? Give feedback.
-
The problem with extension methods is that they are not free - lambdas create a fairly large overhead. However, using an operator as it is done in other functional languages is unlikely to work since C # requires an expression result identifier. Just another option: using |
Beta Was this translation helpful? Give feedback.
-
On disambiguating overloads: I think it would be better to explicitly use a placeholder like "path" |> File.ReadAllBytes // method group or "path" |> File.ReadAllLines(!, Encoding.UTF8) // use ! as placeholder like PowerShell's $_ This would also work with members of piped value, and allow multiple accesses: fileInfo |> File.ReadAllLines(!.FullPath, !.Encoding) |
Beta Was this translation helpful? Give feedback.
-
This feature would play nicely with #95 (instance var nn = torch.Input(10)
|> torch.Linear(256)
|> torch.Relu()
|> torch.Linear(256)
|> torch.Relu()
|> torch.Linear(1); |
Beta Was this translation helpful? Give feedback.
-
Relevant discussion of the feature in other languages on Hacker News with some convincing arguments for it. |
Beta Was this translation helpful? Give feedback.
-
I really hope this doesn't get turned into a language feature. That may be an unpopular opinion, but most proposals solve a problem. What problem is this solving? |
Beta Was this translation helpful? Give feedback.
-
@AlexanderMorou Opinions are great but just because you can't see the problem it solves even though it's well documented in the proposal or you do but wouldn't use it doesn't mean that there isn't one and it's better to counter a fact with a fact rather than opinion, saying that you hope it wouldn't turn into a language feature in my opinion adds nothing useful to the discussion. |
Beta Was this translation helpful? Give feedback.
-
Not to repeat myself, I posted a comment here about why this code style is problematic. |
Beta Was this translation helpful? Give feedback.
-
Why hasn't this been implemented yet? Such a good idea to have pipes in C#. |
Beta Was this translation helpful? Give feedback.
-
How about this? Console.ReadLine() as var line
=> File.ReadAllBytes(line) as var data
=> SHA1.Create().ComputeHash(data) as var hash
=> BitConverter.ToString(hash) as var hashStr
=> Console.WriteLine(hashStr); I'd like to see something like this where you could use pattern matching and have access to the previous results, or perhaps something more similar to LINQ. With possibly |
Beta Was this translation helpful? Give feedback.
-
You don't need this when you are writing applications that use ASP.NET of .NET Framework on the front end, I've written UI using C# for a long while and I never needed a pipe operator. If you do backend stuff that you might find this useful, but because front-end for windows is 80%+ what c# is for they are not going to invest in doing this. And if you are doing backend, there's C++ and other languages you can use, which let you do stuffs like this |
Beta Was this translation helpful? Give feedback.
-
I think my biggest concern with pipe-forwarding is that the ecosystem was not designed for it, so one of two things would need to happen. Either a lot of extra syntax would be necessary to glue between the piped operation and the existing APIs, or a new ecosystem of pipe-friendly APIs would be required. Without either (or possibly both to some degree) you end up with having a lot of expressions that ultimately cannot be rewritten entirely into the piped form and you'd frequently run into situations where you have to decompose back into imperative statements. I also personally don't see that much of a benefit. Sure, you can pack a lot more into what smells like a single expression, but is that really better? In many cases I like having the intermediate assignments to locals, especially since that works a lot better with existing tooling around debugging (which would likely need a bit of work to achieve similar with pipe forwarding to inspect intermediate values). Sure, I have to name those locals, but names are cheap. But I also don't find it at all problematic to have small bits of declarative code within imperative code. Edit: That's even before we get into the requests for multiple flavors of pipe forwarding, including null propagation and awaiting (and the combination of the two). |
Beta Was this translation helpful? Give feedback.
-
There are some new questions above. All about "why do we need it?". Just want to point here - as for me, a pipeline/composition in C# should give the ability for the compiler to Just a 'syntax sugar' with the only purpose - reduce some lines of code - not have big sense. void Main() => x |> A |> B |> C;
int A(x) => x + 1;
int B(x) => x + 2;
int C(x) => x + 3; -> void Main() => D(x);
int D(x)
{
x = x + 1;
x = x + 2;
x = x + 3;
return x;
} |
Beta Was this translation helpful? Give feedback.
-
I would prefer LinqQ style:
I hope the compiler can optimize this. Or at least we need more verbose syntax:
Otherwise it will look like Haskell. |
Beta Was this translation helpful? Give feedback.
-
I quickly wrote this code.
|
Beta Was this translation helpful? Give feedback.
-
I still believe that this operator should merely serve as syntactic sugar, allowing statements to be expressed in the form of expressions . However, it might be worth considering a "quick return" from a pipeline. So far, the only solution that comes to mind is to use the return and continue keywords: async Task<bool> RenameUser(int userId, string newname) =>
await GetUserNameByIdAsync(id)
| var user >
user == null ? return false : continue
|>
LoadSystemPermissions()
| var perms >
await ValidateAccess(user, perms, Actions.Rename)
|>
RenameUser(user, newname)
| user >
Log.Info($"User with id {user.Id} now has the new name {user.Name}")
|> true; which is syntax sugar for the code: async Task<bool> RenameUser(int userId, string newname)
{
var user = await GetUserNameById(id) ;
if(user == null)
{
return false;
}
var perms = LoadSystemPermissions();
await ValidateAccess(user, perms, Actions.Rename);
user = RenameUser(user, newname);
Log.Info($"User with id {user.Id} now has the new name {user.Name}");
return true;
} |
Beta Was this translation helpful? Give feedback.
-
Ported from dotnet/roslyn#5445
Pipe-forward operator
Summary
Lets you pass an intermediate value onto the next method, in the same order that they will be evaluated.
Proposal
When you are chaining multiple methods to one another you might end up with something like this:
Using pipe-forward operator it can be written as (in the same order that they will be executed):
Since C# is not exactly a functional language, forwarding to the last parameter wouldn't be useful most of the time, because not every method parameters are written with a sensible order for currying purposes. Also, in functional languages we don't have overload resolution, that is, functions are often numbered like
iter1
,iter2
, etc. In C#, however, we can utilize overload resolution and optional/named arguments, to be able to use this in a wide variety of use cases without introducing any other mechanism.Argument Lists
Applicability of the argument list in the RHS is roughly defined as follow:
Empty argument list
It's a compile-time error if the method in the RHS doesn't accept any arguments.
Positional arguments
Each positional argument will be matched in order to the list of method parameters. If there was more positional arguments than number of parameters minus one and the last parameter was not
params
, the method is not applicable. Otherwise, the LHS goes to the last element in the expanded form.Optional arguments
In case of optional arguments, LHS goes to the leftmost unspecified parameter which has the identical or implicitly convertible type of LHS. If there was a more specific parameter, then we skip other less specific ones.
Named arguments
Each named argument will be matched to a parameter with the given name. If one of the named arguments failed to match, or matches an argument already matched with another positional or named argument, the method is not applicable. Otherwise, we'll do as above.
The method is not applicable (1) if more than one of non-optional parameters are not specified, (2) LHS was not implicitly convertible to its type (3) or it's a
ref
orout
parameter.Evaluation order
The LHS will be evaluated in the lexical order, i.e. first.
Variations
Null-conditional forwarding
(From dotnet/roslyn#8593)
Function
F
won't get executed if the forwarded value wasnull
, and also,Foo.Bar?.Bar
only evaluates once. Note that the value forwarded to the target functionF
is of a "non-nullable type".Just like
?.
operator, you don't need to use?>
if the target function doesn't return a nullable value, so for chaining you should use the regular|>
operator to not perform an additional null-checking.Syntax
relational-expression:
forward-expression
statement-expression:
forward-expression
forward-expression:
relational-expression
|>
shift-expressionrelational-expression
?>
shift-expressionThe expression on the right-hand-side must be one of the kinds that a value can be forwarded to, namely, invocation-expression, object-creation-expression, an await-expression that contains any of applicable expressions (recursively), etc.
Examples
Beta Was this translation helpful? Give feedback.
All reactions