Sometimes when writing statements in C# you have to take the return value from one method and pass it to another one. When writing this, it often times feels like you are writing it in the wrong order. If you want to read a stream, convert it to a string and then parse it as json, today you would write something like this:
Person p = JsonConvert.DeserializeObject<Person>(Convert.ToBase64String(stream.ToArray()));
When writing this it feels wrong. You start by writing the json parsing, then you convert to string, then you read the stream. It would be much nicer to be able to write it the other way around, which is the way you actually preform the different steps.
Person P = stream.ToArray()
-> Convert.ToBase64String(...)
-> JsonConvert.DeserializeObject<Person>(...);
I think this would be a much more clearer way of writing the above code. Here you get the steps in order. You read the stream, then you take the return value of that and put it into the convert method, you then take the return value from that and parse it as json. In this basic example the “pipe operator” is represented by an arrow (->) and the parameter where the result from the previous method should be inserted is represented by three dots (…), but that may not be the best syntax.
This would also work with methods with multiple arguments.
Person P = stream.ToArray()
-> Convert.ToBase64String(... , Base64FormattingOptions.InsertLineBreaks)
-> JsonConvert.DeserializeObject<Person>(...);
I think adding something like this to allow piping of data from method to method would improve the readability of a lot of code and remove a lot of long lines and “closing parenthesis madness” that often comes as a result of nested expressions.
See also
and kinda
I don't think ->
would work, since that's already an existing operator.
The arrow was just an idea I had. An other possible syntax could be the F# pipe operator |>
. Using that, the syntax would look something like this:
Person P = stream.ToArray()
|> Convert.ToBase64String(... , Base64FormattingOptions.InsertLineBreaks)
|> JsonConvert.DeserializeObject<Person>(...);
This feature is something I've wanted ever since i saw it in F#, but i think it needs to be assisted by partial application and/or currying to make it flow well. Just like in F#. Function composition wouldn't hurt either.
I agree that Function composition and currying would be great to have in c#, but this is a feature that could be implemented relatively easily today.
The conversations around forwarding, currying and partial application are spread around a bunch of issues here. IIRC, the issue with currying is that it is inherently incompatible with overloading, which is generally forbidden in functional languages. F# only permits overloading strictly when writing public methods and in those cases it disallows currying. Pretty much the polar opposite of C#.
While currying might be impossible to implement, forwarding (piping) should be possible to implement. It doesn't have to be implemented in a “functional” was. As long as you specify what parameter to pipe to, it should be able to handle multiple arguments and overloading without making a lot of changes to the languages inner workings and philosophy.
It is amazing to me how often a "pipe pattern" comes up in practical code. It's just a very common thing to do. In fact it's part of why extension methods are so awesome.
A lot, not all, but a lot of this can be done with extension methods today. The issue is they do not work on method groups. If extension methods targeting delegates were made available on method groups then a library could implement this sort of piping. For example, Scala has an andThen
method which works a lot like this.
@aluanhaddad While it is true that you can't invoke an extension method against an method group expression, this is only because the compiler is hesitant to infer delegate types because of ambiguity. You can use a simple convenience method similar to fun
from the language-ext library to get rid of this hassle:
var readline = fun(Console.ReadLine); // Typed as Func<string>
Moreover, when performing function composition you pass method groups as the regular parameters of an extension method far more frequently than you invoke an extension method against them. Referencing the example from my comment in #5445:
// Fine, this is a little inconvenient, but can be mitigated as shown above
Func<string> f = Console.ReadLine;
// From this point on there's no issue with passing method groups
var program = f
.Compose(File.ReadAllBytes)
.Compose(SHA1.Create().ComputeHash)
.Compose(BitConverter.ToString)
.Compose(Console.WriteLine);
That is the technique I was referring to and if there's no ambiguity there then there's no ambiguity with extension methods as there is a simple syntactic transformation between the two forms.
The compiler would need to look for applicable method group conversions using the same rules as it currently does when invoking the helper in your example, which is itself resolved by converting the method group to a delegate.
piping could add .tailcall
We are now taking language feature discussion in other repositories:
Features that are under active design or development, or which are "championed" by someone on the language design team, have already been moved either as issues or as checked-in design documents. For example, the proposal in this repo "Proposal: Partial interface implementation a.k.a. Traits" (issue 16139 and a few other issues that request the same thing) are now tracked by the language team at issue 52 in https://github.com/dotnet/csharplang/issues, and there is a draft spec at https://github.com/dotnet/csharplang/blob/master/proposals/default-interface-methods.md and further discussion at issue 288 in https://github.com/dotnet/csharplang/issues. Prototyping of the compiler portion of language features is still tracked here; see, for example, https://github.com/dotnet/roslyn/tree/features/DefaultInterfaceImplementation and issue 17952.
In order to facilitate that transition, we have started closing language design discussions from the roslyn repo with a note briefly explaining why. When we are aware of an existing discussion for the feature already in the new repo, we are adding a link to that. But we're not adding new issues to the new repos for existing discussions in this repo that the language design team does not currently envision taking on. Our intent is to eventually close the language design issues in the Roslyn repo and encourage discussion in one of the new repos instead.
Our intent is not to shut down discussion on language design - you can still continue discussion on the closed issues if you want - but rather we would like to encourage people to move discussion to where we are more likely to be paying attention (the new repo), or to abandon discussions that are no longer of interest to you.
If you happen to notice that one of the closed issues has a relevant issue in the new repo, and we have not added a link to the new issue, we would appreciate you providing a link from the old to the new discussion. That way people who are still interested in the discussion can start paying attention to the new issue.
Also, we'd welcome any ideas you might have on how we could better manage the transition. Comments and discussion about closing and/or moving issues should be directed to https://github.com/dotnet/roslyn/issues/18002. Comments and discussion about this issue can take place here or on an issue in the relevant repo.
I recommend conversation on this feature request continue either at https://github.com/dotnet/csharplang/issues/96 or https://github.com/dotnet/csharplang/issues/74.
Most helpful comment
The arrow was just an idea I had. An other possible syntax could be the F# pipe operator
|>
. Using that, the syntax would look something like this: