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

Closed
alrz opened this Issue Sep 25, 2015 · 125 comments

Comments

Projects
None yet
@alrz
Copy link
Contributor

alrz commented Sep 25, 2015

When you are chaining multiple methods to one another you might end up with something like this:

Console.WriteLine(BitConverter.ToString(SHA1.Create().ComputeHash(File.ReadAllBytes(Console.ReadLine()))));

Using forward pipe operator it can be written as (in the same order that they will be executed):

Console.ReadLine()
|> File.ReadAllBytes()
|> SHA1.Create().ComputeHash()
|> BitConverter.ToString()
|> Console.WriteLine();

We can take a step further and specify named args (similar to currying proposed in #3171):

Console.ReadLine()
|> File.ReadAllBytes()
|> SHA1.Create().ComputeHash()
|> BitConverter.ToString()
|> Console.WriteLine(format: "SHA1: {0}");

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.

Action<int> a = q => {};
arg |> a;       // ERROR

void M() {}
arg |> M();     // ERROR

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.

void M(int a) {}
arg |> M();     // M(arg);

void M(params int[] a) {}
arg |> M();     // M(new[] { arg });
arg |> M(1);    // M(new[] { 1, arg });
arr |> M();     // M(arr);

Evaluation order: The LHS will be evaluated in the lexical order, i.e. first.

F() |> M(G());  // var t = F(); M(G(), t);

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.

void M(int a, int b, double c = 0, int d = 0) {}
4d |> M(2, 3);  // M(2, 3, 4d, 0);
4  |> M(2, 3);  // M(2, 3, 0, 4);

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.

void M(int a, int b, int c) {}
void M(int a, double b, int c) {}
1  |> M(2,  c: 3);      // M(2, 1, 3);
1d |> M(a: 2,  c: 3);   // M(2, 1d, 3);

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 or out parameter.

Lambda expressions: You can forward to a syntactic lambda if you want to explicitly name the forwarded value, e.g.

Console.ReadLine()
|> File.ReadAllBytes()
|> SHA1.Create().ComputeHash()
|> bytes => { foreach(var b in bytes) Console.Write($"{b:x2}"); };

No delegate will be created and the lambda will simply elided just as you were wrote:

    foreach(var b in System.Security.Cryptography.SHA1.Create().ComputeHash(File.ReadAllBytes(Console.ReadLine())))
        Console.Write($"{b:x2}");

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,

var r = Foo.Bar?.Baz ?> F() ?? false;
var r = ((temp = Foo.Bar?.Baz) != null ? F(temp) : null) ?? false;

Function F won't get executed if the forwarded value was null, and also, Foo.Bar?.Bar only evaluates once. Note that the value forwarded to the target function F 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.

var r = nullable?.Foo.Bar ?? value;
var r = (nullable != null ? nullable.Foo.Bar : null) ?? value;

var r = value |> F() ?> G() ?? value;
var r = ((temp = F(value)) != null ? G(temp) : null) ?? value;

var r = nullable ?> F() |> G() ?? value;
var r = (nullable != null ? G(F(nullable)) : null) ?? value;

var r = nullable ?> F() ?> G() ?? value;
var r = (nullable != null ? (temp = F(nullable)) != null ? G(temp) : null : null) ?? value;

var r = value |> foo?.F();
var r = foo?.F(value);

var r = nullable ?> foo?.F();
var r = nullable != null ? foo?.F(nullable) : null;
@HaloFour

This comment has been minimized.

Copy link

HaloFour commented Sep 25, 2015

You already mention #3171 so you must be aware that this is a dupe, except with -> instead of |>.

@gafter

This comment has been minimized.

Copy link
Member

gafter commented Sep 25, 2015

The symbol -> is already an operator in C#, but the concept could be done with a different syntax.

@tpetrina

This comment has been minimized.

Copy link

tpetrina commented Sep 26, 2015

I love it The code is already sequential and syntax should imply that.
How would you combine this with asynchronous methods?

@alrz

This comment has been minimized.

Copy link
Contributor

alrz commented Sep 26, 2015

@tpetrina That wouldn't be a special case, just an await before the method name will do the job. In case of instance methods you would take advantage of instance method delegates (#5444):

// Not a useful example but just for demonstration:
string result =
    new StreamReader(path)
    |> await StreamReader::ReadToEndAsync

And as @gafter mentioned -> is already an operator so I think the closest alternative for this, is to borrow |> from F#.

@tpetrina

This comment has been minimized.

Copy link

tpetrina commented Sep 26, 2015

Now that looks quite F#ish. Shouldn't you rather write:

string result = await
    new StreamReader(path)
    |> await StreamReader.ReadToEndAsync

Can you do this?

Task<string> resultTask = 
    new StreamReader(path)
    |> StreamReader.ReadToEndAsync
@alrz

This comment has been minimized.

Copy link
Contributor

alrz commented Sep 26, 2015

Now that looks quite F#ish.

That's exactly what I was afraid of when I proposed ->!

The former with two awaits doesn't make sense but the latter is correct. The problem is, when you want to pass the value (rather than Task<> itself) to the next method you should first await it:

new StreamReader(path)
|> await StreamReader.ReadToEndAsync
|> Console.WriteLine
@tpetrina

This comment has been minimized.

Copy link

tpetrina commented Sep 26, 2015

I see, so you men the code above is equivalent to the following:

Console.WriteLine(await new StreamReader(path).ReadToEndAsync());

@gafter gafter added this to the C# 7 and VB 15 milestone Oct 5, 2015

@gafter gafter self-assigned this Oct 5, 2015

@eyalsk

This comment has been minimized.

Copy link

eyalsk commented Oct 13, 2015

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

Console.ReadLine()
.. File.ReadAllBytes
.. SHA1.Create().ComputeHash
.. BitConverter.ToString
.. Console.WriteLine;

2nd Example

Console.ReadLine() .. File.ReadAllBytes .. SHA1.Create().ComputeHash
.. bytes => foreach(var b in bytes) => Console.Write($"{b:x2}");
@RichiCoder1

This comment has been minimized.

Copy link

RichiCoder1 commented Oct 14, 2015

I'm actually going to be contrarian and say I would like and prefer |> over .. and -> as both of those have other very clear definitions in my head coming from other languages.

@alrz

This comment has been minimized.

Copy link
Contributor

alrz commented Oct 14, 2015

@eyalsk .. looks more like a special kind of member access operator like Dart's cascade and does not imply forwarding. Besides, it is better suited for ranges, if we ever wanted to introduce them in the language.

@RichiCoder1 I'm not a fan of copying the exact same syntax from other languages, the reason that F#'s using |> is that it uses pipe in a lot of syntaxes like discriminated unions, pattern matching, backward pipe, etc and |> just makes sense in that context and it's consistent. on the other hand, using it in C# makes it really F#ish.

I suggest :> as an analogous to the named agruments, since the value will forward to a agrument in the next method, so

Console.ReadLine()
:> File.ReadAllBytes
:> SHA1.Create().ComputeHash
:> BitConverter.ToString
:> Console.WriteLine(format: "SHA1: {0}");

In the last line you can see what I mean.

@eyalsk

This comment has been minimized.

Copy link

eyalsk commented Oct 14, 2015

@alrz I understand.

I like this version :> a lot more than the pipe version, to me it looks better.

@aluanhaddad

This comment has been minimized.

Copy link

aluanhaddad commented Oct 26, 2015

Personally I cannot stand code like this:

tuple :> dictionary.Add;

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 Compose and AndThen to these delegate types.

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

@paulomorgado

This comment has been minimized.

Copy link

paulomorgado commented Oct 26, 2015

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 (Func<> or Action<>).

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.

@alrz

This comment has been minimized.

Copy link
Contributor

alrz commented Oct 26, 2015

@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.

@mausch

This comment has been minimized.

Copy link

mausch commented Oct 26, 2015

Please don't use <: or :>, they're standard notation for subtyping in general PLT. It would be awfully confusing to use these symbols for function application.

@alrz

This comment has been minimized.

Copy link
Contributor

alrz commented Oct 26, 2015

@mausch I would like to hear what are you suggesting instead.

@mausch

This comment has been minimized.

Copy link

mausch commented Oct 26, 2015

@alrz I don't particularly care about this proposal as long as it doesn't pollute the language with confusing notation...

@panesofglass

This comment has been minimized.

Copy link

panesofglass commented Oct 26, 2015

@alrz F# and Elixir both use |>, so it would seem you have prior art from which to draw across multiple platforms.

@panesofglass

This comment has been minimized.

Copy link

panesofglass commented Oct 26, 2015

Also, I agree with @mausch. I cannot look at :> without thinking of an upcast. I find that highly confusing.

@mausch

This comment has been minimized.

Copy link

mausch commented Oct 26, 2015

ocaml-core and Scalaz use |> as well.

@alrz

This comment has been minimized.

Copy link
Contributor

alrz commented Oct 26, 2015

@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 |>.

@masaeedu

This comment has been minimized.

Copy link

masaeedu commented Aug 24, 2016

As a side note, once extension everything lands, you could e.g. use the & operator instead of Compose, which reads rather nicely:

var program = fun(Console.ReadLine) 
    & File.ReadAllBytes 
    & SHA1.Create().ComputeHash
    & BitConverter.ToString
    & Console.WriteLine;

program(); 

// Input: consoleapplication1.exe
// Output: FB-EF-53-C8-CE-B4-7D-63-19-BB-E1-3F-22-89-02-CB-1F-0C-00-CA
@eyalsk

This comment has been minimized.

Copy link

eyalsk commented Aug 24, 2016

@masaeedu There are at least two problems with this approach though, isn't?

  1. People need to bloat their code with a library although this is probably solvable by adding it to the framework.

  2. Performance - You need to use delegates and make an extra function call for each function you pass whereas when we have this in the language the compiler can generate the messy and ugly code for us without having these redundant calls.

@masaeedu

This comment has been minimized.

Copy link

masaeedu commented Aug 24, 2016

@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 Expression<Func<...>> and compile whatever you want instead of accepting Func<...> and composing with a wrapping lambda as I am. Any performance optimizations you could envision within the compiler (e.g. composed function inlining) can be achieved by a library author with sufficient expertise.

@eyalsk

This comment has been minimized.

Copy link

eyalsk commented Aug 24, 2016

@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!

@masaeedu

This comment has been minimized.

Copy link

masaeedu commented Aug 24, 2016

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.

Maintenance of libraries is orthogonal to language design concerns. You could request this as a BCL feature or a Microsoft.Extensions.Functional spinoff if you are concerned that no one but Microsoft would maintain the library properly, but sticking a feature into into the language with the rationale that Microsoft will be forced to maintain the underlying implementation is a poor justification IMO. Given the amount of activity on issues such as these, I have very little doubt that there is enough demand and talent for functional C# to maintain such a library.

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 |> operator composes poorly with other functional programming concepts without additional revisions to the language. Lets say we want to pipe to/from functions of multiple arguments. We realize this necessitates currying and partial function application. Now we must decide between:

  • filing issues asking for currying, PFA language features that interoperate with our piping feature correctly and performantly. Expect lots of work, contortions around backward compatibility, and a delay in other features that people want. All for some syntax sugar
  • baking partial application into the |> syntax, with the disadvantage that you can't use these broadly applicable concepts elsewhere
  • writing or importing code that implements these concepts with standard C#, which means we're back to square one, but with the handicap that we may no longer hook into the forward piping implementation as easily for performance optimizations

From my perspective it would be better to e.g. ask for custom infix operators or extension operators than to ask for |>, since the former would compose with existing language features to enable the scenario you're describing, in addition to enabling a number of other frequently requested use cases.

YMMV.

@eyalsk

This comment has been minimized.

Copy link

eyalsk commented Aug 24, 2016

@masaeedu

Maintenance of libraries is orthogonal to language design concerns.

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.

In C#7 we've already ended up with somewhat hamstrung versions of pattern matching and declaration expressions.

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.

Similarly, the proposed |> operator composes poorly with other functional programming concepts without additional revisions to the language.

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. :)

@DavidArno

This comment has been minimized.

Copy link

DavidArno commented Aug 24, 2016

@eyalsk,

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;
@Joe4evr

This comment has been minimized.

Copy link

Joe4evr commented Aug 24, 2016

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 [...]. Instead of downloading libraries, downloading official compiler extensions such as "C# Functional Paradigm", "C# Async Paradigm" where at its core there's "OO Paradigm".

So you want #13322?

@eyalsk

This comment has been minimized.

Copy link

eyalsk commented Aug 24, 2016

@Joe4evr exactly but I wouldn't go as far as externalizing "yield return". :)

@DavidArno

This comment has been minimized.

Copy link

DavidArno commented Aug 24, 2016

@Joe4evr, @eyalsk,

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, unit, rather than void etc, then such a modular approach will simply sideline functional and async features even more than they are at present.

@Joe4evr

This comment has been minimized.

Copy link

Joe4evr commented Aug 24, 2016

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.

@eyalsk

This comment has been minimized.

Copy link

eyalsk commented Aug 24, 2016

@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.

@HaloFour

This comment has been minimized.

Copy link

HaloFour commented Aug 24, 2016

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.

@eyalsk

This comment has been minimized.

Copy link

eyalsk commented Aug 24, 2016

@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?

@forki

This comment has been minimized.

Copy link

forki commented Aug 24, 2016

@HaloFour

This comment has been minimized.

Copy link

HaloFour commented Aug 24, 2016

@eyalsk

Yes but hypothetically say that we had these foundations, do you think it would improve things?

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.

@eyalsk

This comment has been minimized.

Copy link

eyalsk commented Aug 24, 2016

@HaloFour Okay, yeah, you're right. 😄

@niklaskallander

This comment has been minimized.

Copy link

niklaskallander commented Dec 11, 2016

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.)

@alrz

This comment has been minimized.

Copy link
Contributor

alrz commented Dec 11, 2016

@niklaskallander It means "ready to prototype" per Developing a Language Feature.md.

@alrz

This comment has been minimized.

Copy link
Contributor

alrz commented Mar 21, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment