-
Notifications
You must be signed in to change notification settings - Fork 17.8k
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: Go 2: add elixir-like pipe operator #33361
Comments
This operator is closely related to the function composition operator ( It is also closely related to Haskell's monad sequencing operator ( |
In a language without partial function application or currying, the major question is: which parameter is elided if the function accepts multiple arguments? You've chosen to implicitly pass the result(s) as the first argument(s), but if adopted I would rather we have some explicit indicator (perhaps s := strings.TrimSpace(x) |> strings.ToLower(_) |> strings.ReplaceAll(_, "/old/", "/new") |
I like the "no magic" aspect of Go, so treat I also like your suggestion for an explicit indicator. That is even better! |
Lots of functions return errors, so I wonder how much real world code could really use this. |
Here is a library with 1,822 stars just to do this for http middleware. Pipe operator would have the advantage of being compile-time syntactic-sugar rather than runtime. Also, since we don't have operator overloading, pipe operator could really clean up calculations using alternative number types: bignum, complex, matrix, etc... That the mathematicians bothered to coin |
@BourgeoisBear, this may also be an interesting use-case to consider for generics. The val o : ('a->'b) * ('c->'a) -> 'c->'b That has an interesting (lack of) interaction with the the current Contracts proposal, in that that proposal includes neither infix operators nor variadic contracts. Even including the Rust-like lightweight function syntax from #21498, I suspect the best you could do is something like: func Apply(type A, B) (x A, f func(A) B) {
return f(x)
}
func Seq(type A, B, C)(f func(A) B, g func(B) C) func(A) C {
return func(x A) C {
return g(f(x))
}
} with a call site like: s := Apply(strings.TrimSpace(x), Seq(strings.ToLower, |x| { strings.ReplaceAll(x, "/old/", "/new") })) That's not terrible, but it does lack the syntactic smoothness of your proposal. |
@ianlancetaylor , @bcmills , if a Go pipe operator has to support error handling, here is my first stab (more thinking-aloud than wish-list here, BTW): Support optional pipeline-continue parameters ( If the check-expression evaluates to /*
|>
always continues to next stage in the pipeline
|check-expression; fail-result-expression>
only proceeds to next stage if check-expression evaluates to true
*/
// convert string to int, double it, convert back to string...
s, err := "1234" |>
strconv.Atoi(_1) |_2 == nil; "NaN", _2>
strconv.Itoa(_1 * 2), nil
// s = "2468", err = nil
s, err := "wxy" |>
strconv.Atoi(_1) |_2 == nil; "NaN", _2>
strconv.Itoa(_1 * 2), nil
// s = "NaN", err = 'parsing "wxy": invalid syntax'
// strconv.Itoa() is never called |
The most common solution is to instead separate the calls into separate lines. As a function gets more complex, it should expand vertically, not horizontally. Simple rewrites of your examples which fix both of your complaints:s := strings.ToLower(strings.TrimSpace(x)) s = strings.ReplaceAll(s, "/old/", "/new") http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) { f := LogMiddleware(IndexHandler) f = SomeOtherMiddleware(f) RequireAuthMiddleware(f) }) |
@deanveloper, "it should expand vertically, not horizontally" isn't the win here. Elixir-style pipe operator expands vertically just fine. The win is easier-to-read code and less intermediate variable juggling. Elixir style: put_status(conn, :bad_request)
|> put_view(ErrorView)
|> render(:raise, exception: e, trace: __STACKTRACE__) @deanveloper style: t := put_status(conn, bad_request)
t = put_view(t, ErrorView)
render(t, "raise", e, STACKTRACE) The value proposition in Go is actually higher, since variables are typed. What if the type that t := put_status(conn, bad_request)
q := put_view(t, ErrorView)
render(q, "raise", e, STACKTRACE) And those variables are not free of implications! Do they conflict with or shadow any prior variables? Are they used later in the function? The pipe operator sidesteps these implications, and makes it easier to read and write code in a functional style. |
Actually, because of semicolon placement rules, it would have to expand like the following:
I'll agree that variable juggling is an issue, but I personally don't think it's one that needs solving. It's a large(ish) language change to fix a very minor issue. If a variable gets redeclared/shadowed, it either will cause a compile error (which is very easy to see) For instance, what if you want to change your Not to mention that you would have to do this at every single call-site of If you didn't have the pipe operator, the only changes to make is to add Another case: what if you want to print the result of Without the pipe operator? Just add a new line and put in Not to mention that Go isn't exactly a fan of using special symbols. There are very few symbols in Go that have special meaning that aren't common, which I have brought up in other proposals before. Here's a quick list of all of the ones in Go that aren't exactly commonplace in my eyes:
|
This scenario has already been addressed.
Just add another stage with an anonymous function. put_status(conn) |>
func(c Conn) Conn { fmt.Printf("%+v", c); return c }() |>
... |
I must admit that that's an awful lot of code for a simple debugging statement.
I had missed the comment, sorry. The syntax for indexed piping almost looks like Perl to me, which definitely is not desirable. Not to mention that the proposed syntax is a breaking change since |
Use a different syntax then. Just work-shopping the idea here. Was prefixed with "more thinking-aloud than wish-list here, BTW". @deanveloper, try Elixir. I am not recommending it here, so much as sharing that the pipe operator is one thing they got right. Pipes are also pretty sweet in bash. |
This proposal doesn't seem to have strong support. It doesn't provide any functionality that we don't already have, it just permits writing function calls in a different order. Therefore, this is a likely decline. Leave open for a month for final comments. |
Go has function literals/closures and you can trivially write a helper function that does exactly what you want. For instance, with: func apply(s string, fs ...func(string) string) string {
for _, f := range fs {
s = f(s)
}
return s
} you can call There's no reason to change the language when you can just write a trivial helper function that allows you to use the left-to-right notation you are looking for. |
@griesemer, not the same thing. Elixir-style pipe operator composes functions where input/outputs may be of different types through the pipeline (i.e. f(string) int -> f(int) float -> f(float) struct...). Also, pipe operator would be compile-time rather than run time. Golang team has set a high bar for inclusion, and I appreciate that. If this gets shot down for what it is, that's cool. Only answering here to make sure this isn't being shot down for what it isn't. |
@BourgeoisBear Point taken. You can of course also trivially write: t1 := f1(t0)
t2 := f2(t1)
...
tn := fn(tn_1) which is perhaps not as elegant but also reads nicely top-down (and avoids overlong lines if these chains are really long). The effect is the same. Writing the code explicitly also won't raise questions as to which arguments are "flowing in" for things like The overwhelming feedback we hear over and over again from Go users is that the explicitness of Go is what makes the language so easy read and use - even if it comes at the cost of extra typing. A new Go user will know what In short this just doesn't seem to meet the bar for inclusion. It doesn't solve an urgent nor an important problem in Go. |
JavaScript added pipe operator recently but for Golang it doesn't seem a real improvement toward functional programming without some other things. |
There were further comments since this was marked as a likely decline (#33361 (comment)) but they do not change the reasoning. -- for @golang/proposal-review |
I think Elixir's Pipe Operator would be a slam-dunk for Go.
Without a pipe operator, we write things like this:
Two complaints: 1) fussy parenthesis accounting, 2) text reads L-to-R, action happens R-to-L.
With a pipe operator, they could be re-written like this:
It's a tad longer, but easier to read. The operations have more visual separation, and now both text and action flow left-to-right.
Elixir's pipe operator only passes the first parameter. For Go, it would probably make more sense to pipe as many parameters as the previous call returns.
The text was updated successfully, but these errors were encountered: