-
Notifications
You must be signed in to change notification settings - Fork 17.6k
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: introduce a way of performing function chains #56283
Comments
I don't think using
It's not a syntactical ambiguity - it's all just a call expression with a selector expression - but it's at least confusing to read. There would also be the question of what to do if multiple of these are defined - e.g. if there is both a function So while we can use [edit] oh, actually your [edit2] After I also just don't like this proposal in general. I believe it leads to less readable code, not more readable code. It encourages writing long, chained statements and DSLs. I just don't like that style of code and find it hard to understand, personally. But that's just a personal aesthetic preference. |
@Merovius We're not chaining statements but function call expressions, don't you think that this: double := func (x int) { return x * 2 }
overFive := func (x int) { return x > 5 }
add := func (x, y int) { return x + y }
reduceSlice(
add,
filterSlice(
overFive,
mapSlice(
double,
[]int{1, 2, 3, 4, 5},
),
),
) is a lot harder to understand than this []int{1, 2, 3, 4, 5}
-> mapSlice(double)
-> filterSlice(overFive)
-> reduceSlice(add) I'm not suggesting anything here, just trying to explain that having a nice way of doing function composition together leads to more readable code, I'm also aware someone can also take the first snippet and do this to it double := func (x int) { return x * 2 }
overFive := func (x int) { return x > 5 }
add := func (x, y int) { return x + y }
doubled := mapSlice(double, []int{1, 2, 3, 4, 5})
filtered := filterSlice(overFive, doubled)
result := reduceSlice(add, filtered) but this approach introduces new problems:
|
My position is that neither is particularly readable. My position is that this should be put into its own function, named by whatever it is actually trying to achieve (your example is too contrived to take a stab). At that point, I don't think the syntax matters very much. That's basically my point. Your mileage may vary. |
Since Go does not allow function names to start with a number or currently support anything where a floating point number can be defined with a constant and variable part, like 3.x or x.5, I do not believe that this is an issue. I hadn't seen the Also, @Merovius, you and I have already discussed the use of the . operator here heavily, and I tend to agree that it would introduce real ambiguity. I personally lean towards And @MSE99, I really like your choice of example here: []int{1, 2, 3, 4, 5}
-> mapSlice(double)
-> filterSlice(overFive)
-> reduceSlice(add) as it was similar to the ones I was testing when I first started this proposal. To give Go credit, this is almost possible now with generics, if methods could be generic it would work with |
Don't forget hexadecimal floating point number literals like |
Regarding the case of defining the function These are more good reasons against using the |
And regarding the taste discussion that @Merovius has brought up, I find another area more compelling, I think that for a large number of Go developers (just maybe not the ones on the GitHub forums) such a syntax would be valuable and highly satisfying. I understand that for some this is antithetical to their taste, and that similar proposals have ranged from negative to overwhelmingly downvoted. I also want to point out that the post requesting type parameters on generic methods was around 229 upvotes and 7 downvotes last time I checked. To me, this says that people want to do these things, just with methods instead of functions. If the official opinion of the Go contributors and core team is that generic methods are impossible given the architecture of Go, and that generic methods are highly desired by the Go user community, we should pursue some way of giving people that functionality. My proposal is one option, there may be others, such as actually solving the problem through the type system. I am not asking for my exact vision to be merged into the language. I am asking for Go to provide some way of chaining generics for the purposes shown in my proposal. I believe that my proposal is the most flexible of the options I have seen, since it supports calling functions chained on primitive values as well as something that looks very close to default final methods for interfaces. For some, this flexibility may be unattractive or seem unnecessary, which is a perspective I can't argue with. But I think it is clear that Go developers are trying to chain generics, and it isn't going well. |
A while back there was lots of discussions on how to improve error handling, i think with this proposal we can simplify error handling by using a type Result[T any] struct {
err error
val T
} we can then use this like: parseToken(r)
-> extractUserID()
-> loadUserFromDB(ctx, db)
-> authenticate()
-> sendLoginAlert(ctx)
-> handleFailure(ctx) Every function in the pipeline above accepts and returns a Result struct, if the result contains a non-nil error it won't do anything and will simply return a new result object with the same error, if the result isn't an error it will do it's processing and wrap the result inside of a new result object, the last function |
@MSE99 - I don't necessarily like (or dislike) this, but this is a very cool thing that looks nice with this syntax. I actually think it is really cool that you can have func (r Result[int]) loadUserFromDb(...) {
//
} |
@MSE99 Another way to look at it, though, is that the syntax is significantly less useful if we don't have a pretty widely used Also, I don't generally think |
Thank you for pointing out #47680. It is roughly exactly adding a new operator |
Personally my mileage does vary. I have written a modest amount of things like FlumeJava (paper) where 5-6 functions being chained is pretty normal. The types got long and often get in the way more than they elucidate, and naming intermediate results is not more readable. To be fair though, I have also seen aggressive applications of |
Nope - nothing that I know of has changed. To be fair, I don't think that issue was closed for a specific fault, just a lack of interest. Since then, there has been considerable discussion around function chaining in the generic methods thread. I opened this to separate those conversations since there is no other place that I know of for such discussions right now. I would like to use this to get an answer of either "yes, we will work on a way of chaining function calls" or "no, we will not do that, but we are planning to figure out generic methods". Either of those answers address my concerns. |
To be clear, if the answer is "we don't want UFCS and as far as we know generic methods are infeasible", you should be prepared to accept that as well. |
Just a repeated nitpick - what I am proposing is not UFCS. And that's fine, for me though it would still beg the question of how to satisfy everyone requesting generic methods. |
My point is that the answer "they won't be satisfied" must be acceptable. Just as people who wanted generics had to stay dissatisfied for ~10y of using Go. If we can't figure out how to solve a problem satisfactorily, we will err on the side of not solving it. That's how Go is generally developed. |
Ok, sure. I'm just saying this is a practical change that enables a lot of useful functionality while not requiring years of waiting and major changes. If that isn't the Go philosophy, that's fine, but I believe such an approach turns people away - I can't think of another mature programming language that gives me the answer of "no, you just can't do that." |
I'm not as sure about that. One example that comes to mind for me is Python refusing Tail Call Optimization, even though many people are asking for it. To me, that's basically what language design is all about - making choices, to build a coherent product. As I said, there is precedence. We told people for 10 years that they just can't use generics, unless and until we figure out how to implement them. We can tell them for another 10 that they just can't use generic methods, unless and until we figure out how to implement them. It's not a satisfying answer, but sometimes that's just how it is to work collaboratively in an open source project. |
Yeah, that's a fair example, albeit one that I would like to be added to Python. I think we both have our opinions here so I suppose we can accept that. I think this case is different from generics because there is a path forward with little work required but you (as well as others) find it distasteful enough that you prefer to wait for an alternative. That's personal preference, and I appreciate that we can both be open about where we stand. |
The status quo of generic functions + intermediate variables do cover the requested use cases and functionality for both this proposal and generic methods on #49085. There does seem to be a problem that this is not a more obvious conclusion. It is cheaper to write tutorials and examples explaining that this pattern can be used instead of generic methods than it is to adopt and support new syntax. Education about generic functions vs generic methods and then 'wait-and-see a couple of years' could make a good deal of sense. |
Ok, true. Replace functionality with expressiveness or readability. |
I think it's clear that we will not adopt this proposal using dot as the syntactic marker. Dot is heavily overloaded in Go. We don't need to overload it further, especially given that the result is going to look ambiguous to the reader even if it is not technically ambiguous.. Of course this concern does not apply to other syntaxes. That aside my concern is that in practice many Go functions take a |
If I were to submit a few examples of what error handling/multi-return could look like in this thread, would you be open to exploring this proposal and similar ones? Or is your general opinion just that this sort of function chaining is not a good fit for Go and is never going to happen? Don't take that with an edge, it's hard to express tone over text, I'm just pretty busy with work but would spend some time drafting error handling syntax for this if I believed there was a possibility that it would be implemented. |
These language change proposals are evaluated by a committee, and I don't make final decisions myself. That said, I think this proposal is a long shot. The way that a proposal like this might be added to the language would be if lots of people started calling for it, and if they showed existing Go code that would be made significantly clearer or simpler or safer by adopting this change. Right now I see two thumbs up and two thumbs down in the emoji voting on the issue. That's not compelling. Disregarding generics which are kind of a special case, I think the last syntactic change we made to the language was support for new kinds of numeric literals in Go 1.13 (https://go.dev/doc/go1.13#language). That covered issues like #19308, which has 226 thumbs up and relatively few thumbs down. That is the kind of enthusiasm we would expect to see before adopting a syntax change. |
That makes sense, thanks for the background. I truly wish the Go core team the best in finding a way to support generic methods, and if more conversation ever comes up around function chaining I would be happy to get involved since I think it is apparent that this change is important to me. |
I think if one wants to "chain" functions smoothly, one would need to design the function signature for this. Let's say we want to map a
The Collection[S] would need to remember error results and the Collection[T] would provide Chaining with these constraints while trying to adhere to existing coding styles would result in something like:
It would work, but it would lead to much less readable code compared to the status quo. I am not too worried about the prefix of What worries me the most is that very odd (I was actually kinda surprised how poorly this went once I started adding arguments.) |
似乎有点影响代码可读性 |
Regarding the comment from @Linkangyis which I just Google Translated, I find that more true with the |
Based on the discussion above, this is a likely decline. Leaving open for four weeks for final comments. -- for @golang/proposal-review |
No further comments. |
Author background
Experienced with less knowledge of internals.
Java, Elixir, Python, JavaScript, Lisp and some other ones.
Related proposals
Yes, I believe this relates to at least three previous proposals:
proposal: spec: allow type parameters in methods #49085
proposal: Go 2: alternate function call syntax to improve ergonomics of chainable top-level functions #47680
proposal: Go 2: add elixir-like pipe operator #33361
This proposal requests that the question of function chaining be readdressed in the context of generics, considering several syntactic limitations and numerous requests for a reconsideration in the type parameters & methods thread which I have been involved with. In terms of specific differences, this proposal leaves implementation open ended and serves to be a dedicated place for discussion of a solution to the problem of function chaining in Go. This proposal also provides a much more diverse set of new features that would be supported by this syntax.
It shouldn't, unless implementation settles on some "in chain" style of error handling.
At the very least it is tangential, since generics have enabled Java style stream APIs and Ruby style "expect" APIs, both which are severely kneecapped not by generics but by Go's syntax.
It proposes an alternative way of providing what people are looking for with type parameters on methods, which is something that seems impossible given the current implementation of generics.
Proposal
Provide some way of chaining functions, either using a new operator such as
->
,|>
, or..
or by using the.
operator used for calling a method to also call functions.Here I will list a number of potential use cases that would be possible with this simple, backwards compatible change. Here, treat the dot as whatever operator or strategy is settled upon. These are simply meant to show abstract representations of use cases.
numbers.map(toString)
numbers.reduce(add, 0)
expect(foo).toBe(bar)
assertions.3.plus(3)
n.times(print, "Hello")
"hello world".split(' ')
.
for both methods and functions, but if not, here is an example:It is true that several similar issues to this one seem to have been poorly received. The code examples above are in fact possible using nested function calls, which is a point that is quickly made. However, I believe that there is a reason many programming languages are supporting similar syntax, which is that the code ends up closer to readable English and requires less brain power to manually parse. I will quote two people very close to me here:
-My friend.
-Myself.
Finally, I believe that most of what is demonstrated here with syntax would also be doable with generic methods. Three reasons I am not focusing on that:
I believe that this provides amazing value as a change, it adds much more readable syntax to any transformations on data without changing internals. Most importantly, it adds support for a number of programming patterns that are terribly underserved by the current nested function call syntax. The ability to define pseudo-default methods on interfaces is interesting to me, and I imagine more satisfying cases will emerge from this syntax.
For people who take the valid opinion of "why introduce a change that doesn't add any new functionality to the language?", I will say:
A function defined as
plus(a int, b int)
could be called either asplus(3, 4)
or3.plus(4)
. However, instead of the dot operator, I am looking to open discussion as to an appropriate way to signify this function chaining operation. Formally put, I propose that a syntax for calling a function starting with its first argument, then the function name, then the remaining arguments (if any) inside parentheses. I believe this would be achievable without any changes other than to the preprocessor, which could map this syntax back to nested function calls.I would like to use this issue as a place to discuss three main things: the proper operator to represent such a chain, how to appropriate handle errors in such a context (discussed in the previous function chaining issues), and how to (if at all) handle cases such as either multiple returns in the chain and cases where the starting argument should be inserted in a position other than the first one.
There would be two ways to call a function, and possibly a new operator in this context.
You could call any function starting with its first argument instead of starting with the function name.
Yes.
This overlaps with function calls, and nested functions. This would not change how they are handled, but would provide alternatives to calling them in this fashion in specific cases.
No.
Costs
I believe easier for a few reasons. Reading code with these "pipes" would be far more linear for a new programmer, and people with very little programming background would likely be well served by code that looks closer to plain English. (or their native language) I believe this would heavily support much more developer friendly APIs which would make writing Go easier, and it would save people from cases where they want to do something and find out there is no way, which is very grating to...certain people.
Making changes to the preprocessor in the compiler to convert chained function calls into standard function calls.
I am not certain, I am tempted to say none, but anything that statically analyzes Go and doesn't have the latest version to parse it with would not understand this syntax. I think this is true of all language change proposals though.
Likely very very small.
None.
I should be able to throw together a little parser for this if people are interested.
No.
Finally, I want to comment on the first comment on another similar proposal. I saw it suggested that this whole problem could be remedied by allowing variables to shadow each other, while I think this may help it is a greater internal change than this, and it still does not support the concise and inline style of this change, while also introducing the line noise of
foo :=
for every transformation.I know this proposal is very similar to others, but I request that it be considered independently and with an open mind. I am intentionally trying not to limit this to any specific implementation that I have in my head, I want to take feedback from the community. I have provided some things that this supports that I find interesting - I would love others to continue to provide more of these. If anyone else has different changes that they think would support the same things proposed here, I would also love to hear them.
The text was updated successfully, but these errors were encountered: