-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Forward Pipe Operator #5445
Comments
You already mention #3171 so you must be aware that this is a dupe, except with |
The symbol |
I love it The code is already sequential and syntax should imply that. |
@tpetrina That wouldn't be a special case, just an // Not a useful example but just for demonstration:
string result =
new StreamReader(path)
|> await StreamReader::ReadToEndAsync And as @gafter mentioned |
Now that looks quite F#ish. Shouldn't you rather write:
Can you do this?
|
That's exactly what I was afraid of when I proposed The former with two
|
I see, so you men the code above is equivalent to the following:
|
I really like the idea, finally a way to write simple things in a simple manner. :) The second syntax |> isn't as great as -> in my opinion so maybe double dots? 1st Example
2nd Example
|
I'm actually going to be contrarian and say I would like and prefer |
@eyalsk @RichiCoder1 I'm not a fan of copying the exact same syntax from other languages, the reason that F#'s using I suggest Console.ReadLine()
:> File.ReadAllBytes
:> SHA1.Create().ComputeHash
:> BitConverter.ToString
:> Console.WriteLine(format: "SHA1: {0}"); In the last line you can see what I mean. |
@alrz I understand. I like this version |
Personally I cannot stand code like this:
And I find this idea even more distasteful: var sumOfEvensSquared =
xs :> Enumerable.Where(x => x % 2 == 0)
:> Enumerable.Select(x => x:> Math.Pow(x))
:> Enumerable.Sum; I would like to see better support for function composition in C#, and if extension methods could be applied to the majority of Method Groups, perhaps via a costly but useful implicit conversion to a Func or Action, a lot of this could be accomplished nicely as a fluent DSL by adding extension methods like Anyway, If something like this is adopted, please do not go with this F# style syntax. It does not fit well into the language and frankly not even all F# enthusiasts find it readable. See: http://fsprojects.github.io/FSharp.Core.Fluent/ Of course, this is just my opinion, but many of F#'s syntactic constructs are not a good match for integration into C#. This is partly due to the fact that F# was designed to be a "Functional First" multi-paradigm language, and partly due to the fact that the OCaml syntax is radically different from C#. Anyway, I think Scala is a better language from which to draw inspiration when adding functional style features to C# because it was designed to merge FP and OOP. Just as food for though It's also interesting to consider Douglas Crockford's lecture series on Monads where he argues that Monads are easier to understand using method notation. It's quite interesting: https://www.youtube.com/watch?v=dkZFtimgAcM |
Function composition would be more powerful than the limited query operators in LINQ. I think it would even eliminate the need for LINQ. I don't envision another way of composing functions other than using delegates ( A chain of compositions on the same statement could be optimized to a single delegate, but across multiple statements, I don't think it could. |
@aluanhaddad You are not supposed to use it wherever you can, but in some cases it causes to save some temporary variables along the way without sacrificing readability. And don't even compare this to "function composition" it's nothing like that. I don't understand how are you suggesting that "function composition" could be more useful than this in C# while you're mentioning that it's not "functional first" in the next sentence. |
Please don't use |
@mausch I would like to hear what are you suggesting instead. |
@alrz I don't particularly care about this proposal as long as it doesn't pollute the language with confusing notation... |
@alrz F# and Elixir both use |>, so it would seem you have prior art from which to draw across multiple platforms. |
Also, I agree with @mausch. I cannot look at :> without thinking of an upcast. I find that highly confusing. |
ocaml-core and Scalaz use |
@mausch @panesofglass Yes, that was first notation that I could think about, but don't you think it makes it really F#ish? That's all that I'm afraid of, otherwise, nothing's wrong with |
OK, I finally found some free time to finish the first part. Unfortunately, I didn't like @alrz's design, so this is what works now:
Planned:
A question to the readers: how important is @alrz's proposed evaluation order to you? If you write Or, should
|
@orthoxerox yeah, I'd expect to see "Foo" printed first, meaning, execute p.s. Good job. :) |
Forward pipe operator is something I would definitely be glad to see in the language. I keep finding my C# code looking more and more functional these days, and the lack of the forward pipe is starting to hurt. However, I'm not a fan of the way the original proposal by @alrz tries to deduce which RHS argument the LHS result should be passed to. There are three main reasons for this:
I think a way to explicitly reference the LHS value is a better way to do this. @orthoxerox's suggestion of using void LogSomething()
=> GetSomeTuple()
|> tuple => $"[{tuple.Category}] {tuple.Items.Count}"
|> text => Log( LogLevel.Debug, text ); The main benefit would probably be the way to combine pipe with tuple deconstruction: void LogSomething()
=> GetSomeTuple()
|> ( category, items ) => $"[{category}] {items.Count}"
|> text => Log( LogLevel.Debug, text ); But this syntax would also allow the use of meaningful names for the piped arguments (which would be nice for the code readability if you have a bunch of pipes in a single expression) and I think it looks more familiar and C#-like than a magic |
I don't see why you need extensions to the language for this when it can be implemented quite easily in a library. Consider the following simple extension methods:
Now you can write:
The only thing that could make this a bit nicer is delegate type inference to avoid the explicit
You can of course write your own simple implementation of Somewhere in this thread I think I saw someone discussing how to include functions of multiple arguments in this pipeline. Once again, use standard functional composition tools like currying and partial function application here rather than inventing new operators (both are easily implemented in C#, although could be made nicer with variadics). I wish C# allowed custom operators so I could invoke |
As a side note, once extension everything lands, you could e.g. use the
|
@masaeedu There are at least two problems with this approach though, isn't?
|
@eyalsk Better bloat your code than bloat the compiler. Not everyone needs this, and adding a nuget package doesn't really bloat anything besides your project.json. Regarding performance: you can accept |
@masaeedu "Better bloat your code than bloat the compiler." -- your opinion! and I respect it but I strongly disagree, the language needs to help me express logic and if I want to use a functional paradigm to do it I don't need to download a library or anything to do so, I expect it to have this baked in. What I really want to see in a future version of C# is a feature that will allow us to select one or more programming paradigm profile where you use only what you need, this will help solve these issues where the more the language gets richer the compiler gets bloated but in practice, again, the language needs to allow me to express things in natural ways and the compiler needs to adheres to that. Instead of downloading libraries, downloading official compiler extensions such as "C# Functional Paradigm", "C# Async Paradigm" where at its core there's "OO Paradigm". I don't have issues with downloading 3rd-parties libraries but the biggest issue for me is maintenance and support, people write libraries, abandon them and I either need to maintain them myself or find another. I already got bitten by this multiple times but no more! |
Maintenance of libraries is orthogonal to language design concerns. You could request this as a BCL feature or a IMHO the language should provide powerful primitives that compose well in the hands of the developer, rather than continually pushing back work on these to provide narrowly scoped features. In C#7 we've already ended up with somewhat hamstrung versions of pattern matching and declaration expressions. On their own they solve a single problem well, but they compose poorly with each other, and with other functional programming concepts. Similarly, the proposed
From my perspective it would be better to e.g. ask for custom infix operators or extension operators than to ask for YMMV. |
Indeed, they are but I was speaking about having this feature as part of the language vs using a library, I didn't say that this is THE reason we should add it to the language.
Can you give an example to that? I mean what exactly do you mean? I know it's not as complete as you'd expect in other functional languages but iirc they stated somewhere that work on both of these things is still going after C# 7.
Well, I agree that this can lead to more issues and it's likely that it will open a new can of worms but personally, I really like the succinct syntax over a function call because to me and this is purely subjective, it reads better, even though I had different ideas about the symbol itself, however, like you said adding a feature that will allow us to add a custom operator can help tremendously to have the best of both worlds, however, this can also open a new can of worms so really I'm torn on this. I'm not sure whether it was proposed before but maybe you can write a proposal about this custom operator, it seems pretty interesting. :) |
The syntax for such custom pipe operators could be something like: public static TR new operator "|>"<T, TR>(T value, Func<T, TR> func) => func(value);
public static void new operator "|>"<T>(T value, Action<T> action) => action(value);
public static TR new operator "||>"<T1, T2, TR>((T1 p1, T2 p2), Func<T1, T2, TR> func) => func(p1, p2);
public static void new operator "||>"<T1, T2>((T1 p1, T2 p2), Action<T1, T2> action) => action(p1, p2);
.... And for a case of: string F(int p1, string p2) => ...
string G(string p1, string p2) => ...
int H(int p) => ...
int I(int p) => ... The following would then be equivalent: G(F(1, "2"), "3");
((1, "2") ||> F, "3") ||> G;
H(I(1));
1 |> I |> H; |
So you want #13322? |
@Joe4evr exactly but I wouldn't go as far as externalizing "yield return". :) |
The problem I see there is that 'at its core there's "OO Paradigm"'. 😄 Unless there were a way of having the core C# compiler be composed of very few keywords, and almost no assumptions of behaviour, so that I could for example load in the functional extension and have immutable-by-default, |
I agree. Not to mention that supporting such a "modular compiler" model in the first place would very probably require another major rewrite of the compilers and IDE integration and all the other stuff that comes with it. |
@DavidArno Well, yeah, it might be a problem but I don't work on the compiler to actually know how difficult is to refactor the different components to make this so I just stated an opinion of how I think it should be composed. :D @Joe4evr Maybe, I don't really know but it seems really, really odd to me that they need to rewrite everything from the ground up, what's the point of engineering a software system that can't change? systems shouldn't be rigid. This is just my point of view but from where I'm coming from modularity is a feature like any other, you need to introduce it when there's a need for it so saying that they need to rewrite everything is really unlikely but refactoring is more probable. |
The very concept of modularizing C# into a series of pluggable dialects is an awful one. The nature of the support surface for such a beast would be monstrous, to be generous. The CLR was designed to be mostly language agnostic and there are numerous languages that already offer support for these kinds of features in a way that is internally consistent. I'd suggest using them. Perhaps one area of improvement would be in polyglot solutions. If it would be easier to comingle F# and C# then some of these concerns I think would go away. But to try to morph C# into F# while keeping it C# or having it be C# or F# based on some project or file settings is just ludicrous. |
@HaloFour Yes but hypothetically say that we had these foundations, do you think it would improve things? or it would just make things redundant and complicated for the consumer that is for us? |
@HaloFour but but but: https://www.youtube.com/watch?v=Hd9Z9s4_DII |
I don't think such "foundations" are remotely possible in a programming language. Could you imagine the design process that would be required in order to ensure that syntax changes in one dialect doesn't completely break syntax in another dialect? It's tricky and time consuming enough when there isn't a separate Cartesian product of external possibilities to worry about. As for a consumer, could you imagine trying to consume two different "C#" libraries that were each designed to be used from entirely different paradigms? I think it'd be a nightmare. It's bad enough already having to bridge the gap from Scala or F# libraries that were clearly not designed to be used from Java or C# programs. |
@HaloFour Okay, yeah, you're right. 😄 |
I see that that this proposal (along with a bunch of other interesting ones) are marked with the label "2 - Ready". Is that an indication of there being a finished specification of the feature somewhere that you can have a look at? Or is a specification one of the products of the implementation phase (as implied by the label "3 - Working" )? (I had a look at: https://github.com/dotnet/roslyn/wiki/Labels-used-for-issues, but I found nothing satisfying my curiosity.) |
@niklaskallander It means "ready to prototype" per Developing a Language Feature.md. |
Moved to dotnet/csharplang#96 |
When you are chaining multiple methods to one another you might end up with something like this:
Using forward pipe operator it can be written as (in the same order that they will be executed):
We can take a step further and specify named args (similar to currying proposed in #3171):
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 currying and overload resolution at the same time, that is, functions are often numbered like
iter1
,iter2
, etc. In C#, however, we can mix and match overload resolution and optional and named arguments to be able to use forwarding operators in a wide variety of use cases without introducing any other operators.Applicability of argument lists in RHS will be defined as follow:
Empty argument list: It's a compile-time error if the method in RHS doesn't accept any parameters. I suggest the argument list to be not optional, otherwise it will be inconsistent when we actually do have an argument list. RHS will be not evaluated as a general expression, so if you want to forward to a delegate, you will be required to write parentheses in front of it.
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.Evaluation order: The LHS will be evaluated in the lexical order, i.e. first.
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.Lambda expressions: You can forward to a syntactic lambda if you want to explicitly name the forwarded value, e.g.
No delegate will be created and the lambda will simply elided just as you were wrote:
Null-conditional forwarding operator
(Moved from #8593)
It has been suggested by @HaloFour to extend this with a variant that function like the null-propagation operator, as an alternative to #5961,
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 (#5032).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, e.g.The text was updated successfully, but these errors were encountered: