-
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: add become statement to support tail calls #22624
Comments
This Go Forum topic addressed a question on functional programming. I'm curious if there has been consideration of the use of functional programming language shells with Go. http://nurmi-labs.blogspot.com/2016/03/es.html http://nurmi-labs.blogspot.com/2017/04/xs.html The es(1) and xs(1) man pages are similar. BUGS ... The interpreter should be properly tail recursive; that is, tail calls should not consume stack space. http://nurmi-labs.blogspot.com/p/about.html See: the blog posts on es (influenced by Scheme, and Tcl) and xs http://wryun.github.io/es-shell/ BUGS can be fixed, the man pages' cited tail calls. Tail call optimization additional links presented therein possibly of historical interest https://blog.golang.org/first-go-program the program parses and prints an S-expression The es and xs shells can be extended, and notably the latter added a configure option as "--enable-lisptrees". xs is written in C++, and I've expressed interest here in seeing code |
Become comes from Newsqueak, of which Alef was a descendant. By the way, Newsqueak had only become; it had no return statement. And it was aggressive about tail call compaction. From the informal spec: The
is legal only inside a
If |
@forskning This is not the place to discuss functional shells. @robpike Thanks for the correction, didn't come across that. |
@robpike If I read you comment correctly, |
@l3x introducing a new keyword would very likely break some existing code. Stack frame reuse is likely to lead to some issues/confusion, especially when debugging, regardless of how it is implemented. And the implementation would probably be complex and fraught with GC-related issues similar to #22350 So, no, this is definitely not "low impact". |
If this is adopted, I'd like to propose that "become" be spelled "goto", and that it be restricted to function calls, not arbitrary expressions. This has some precedent in perl's Rob's example would look like this:
|
@huguesb Yes, the become statement is a form of tail call optimization. However, the usual definition of TCO is that the implementation applies it as it chooses. Instead, in Newsqueak (and in Alef when become was used instead of return), the "optimization" was always done. That is, the stack frame was always overwritten when the become's expression was a function call. I believe @ianlancetaylor is proposing what Alef did, which is controlling the action according to which keyword is used. |
It's an interesting solution. It could be quite useful for a project I'm working on, actually. I've got two questions, though:
|
Yes, I am proposing using two different keywords, one ( @magical The suggestion of @DeedleFake Deferred functions would be handled as usual, which is to say that they would remain on the defer stack and would be executed when the tail-called function returns. That is, if you write Similarly, writing @l3x @huguesb This proposal would be fairly low-impact in the sense that it would not affect any existing code that does not use |
IMHO: @ianlancetaylor Perhaps, a better name would be |
How would it work with multiple return values? The only way I can think of is it's always I think However,
is uncomfortably close to
in terms of edit-distance, at least. It would be easy to read one as the other when skimming or when you've been debugging a little too long without a break. Likewise, accidentally writing one instead of the other would create a "fun" bug to catch. If that's a serious ergonomic concern, I suppose one way to get around that would be to double up on the keywords and have to write Perhaps it would be better to create a new keyword instead of trying to C++ it in. As far as stack traces, I understand that a common trick in languages with tail calls is to create a circular buffer of traces when entering the first tail call. It allows collection of the last I'd like this, but it seems it would require rather involved changes to the runtime. TCO is easily simulated with a trampoline without those changes, so I don't know if the benefit is worth the cost. |
I don't expect this would typically be a problem, because we'd define I'm still not sure how we would actually implement this, the stack surgery is tricky. |
@jimmyfrasche |
So |
I don't like this idea. Semantically, In addition, I think it would be hard to explain clearly to anyone not familiar with this topic - especially beginners - when they should use one construct or the other, and why. Almost every other feature of the language, by contrast, has a fairly clear-cut use case. I think Go should either support TCO without any special syntax - and with whatever instrumentation is needed to give good stack traces, if it's truly that important - or not have it at all. TL;DR: TCO is an implementation detail, and Go should treat it as such if it implements it. |
@dpinela, Go also values debuggability, and automatic TCO destroys stack trace information. One argument for |
@bradfitz Understood, but there's a question how far one should go for that. Is it more important to provide all stack info, or keeping the number of language constructs simple? I'd be ok with lost stack information as long as it's clearly identified as TCO related. (There's also the option of trying to emulate that stack transparently, but that's a whole different ballgame.) |
Another possibility is allow TCO by default for |
Does |
@randall77 That's what I am suggesting. One could effectively obtain the same result by wrapping the function body in a loop and do the TCO "manually". Maybe there could be a counter showing how many frames have gone "missing" but I suspect that in most cases we'd be just fine. In short, I think the compiler should do TCO under the covers where it's easy, and not bother otherwise. |
Making tail-call optimization implementation-dependent seems worse than not doing it at all. The point of TCO is to ensure that tail calls are safe, not just fast, and if it's safe sometimes but not others (or changes from release to release or from compiler to compiler), we'll end up in a state in which tail-recursion is technically possible but not portable: where some libraries will assume that it occurs and break badly when it does not. That is: the difference between O(1) and O(N) behavior seems much more significant than most optimizations, which instead tweak the constant factors on behaviors with the same asymptotic behavior. |
@randall77 I'm not sure an obscure workaround is better than having an "obscure" feature in the language. For a reader, that construct would seem pointlessly redundant; it reminds me of the trick for getting a closure to properly capture a loop variable, which is similarly non-intuitive even for the writer, let alone the reader. |
@bcmills If the spec says that a self-recursive call in a return (the easy case) is translated via TCO (or TCElimination, perhaps more precisely), it seems pretty well-defined and probably covers 90% of the ground. |
On Wed, Nov 8, 2017 at 10:40 PM, Robert Griesemer ***@***.***> wrote:
@bcmills <https://github.com/bcmills> If the spec says that a
self-recursive call in a return (the easy case) is translated via TCO (or
TCElimination, perhaps more precisely), it seems pretty well-defined and
probably covers 90% of the ground.
But the self-recursive case is the least interesting one. it adds little
expressive power to the language. A generally available TCE instead opens
the way to new programming styles and would really add to the expressivity
of the language.
I'm rooting for this feature!
|
@griesemer every time I've wanted this it was for state machines not converting a recursive function into a loop (though that can be clearer sometimes, certainly), so only self-recursive would cover effectively 0.05% of the ground for me. Tail call elimination is an optimization the way using a more appropriate data structure is an optimization. I wouldn't expect the compiler to say "you used a slice here but you really meant a map so I'm just going to change it for you—you didn't care about the order, right?". Making it explicitly opt-in with a keyword means
But given how easy it is to simulate this feature vs. how hard it is to implement in the language in any form, I'm not really sure it's worth a slot on the limited Go2 feature list. If it does make the cut I'm 👍 on being explicit. |
Because some things are significantly easier to deal with recursively. If |
And |
Please forgive my impudence, but I sincerely hope these kinds of features don't make it into the language. Not under the pretense of convenience, and certainly not under the pretense of performance, seeing as we're talking about optimizing away a few stackframes at best. |
And an unbounded number of them in the worst case. |
Clarification: Go's apparent simplicity is both invaluable and fragile. It would only take a few features to ruin this quality. The Go team has already shown incredible restraint and conservatism, which often leads to better solutions down the road. Would be nice if this continued. |
@cznic As I understand, the problem lies in mutually recursive algorithms? Is there really no other way to express them stack-free? |
Eg.: |
As much as I ❤️ functional programming, one can make a solid argument that for functions that simply recur into themselves, TCO is no better than a simple I apologize if this is irrelevant or derails the discussion, but... Haskell provides a fascinating example demonstrating what happens in a language without I think it demonstrates the relative value of recursion and loops quite convincingly. Aside from mutual recursion, which indeed can be hard to avoid for some algorithms. |
@mitranim, @cznic, @DeedleFake, this exact conversation has already occurred several times in this thread. Let's move further discussion to Slack or golang-nuts or somewhere that doesn't spam other people subscribed to this bug. (But yes, Go will continue to value simplicity.) |
Please stop recommending Slack for these sorts of things. There are lots of contributors who are not on Slack and can't be on slack for legal reasons, accessibility reasons, etc. that may want to follow this conversation. Use the mailing list, to my knowledge everyone can find an email provider to receive emails. |
http://www.stroustrup.com/hopl-almost-final.pdf 4.1.2 The STL emerges Interesting story by the way on the background of the eventual implementation of the Standard Template Library. |
So having gleamed over the discussion I take away a few things and have a few questions. In newsqueak there is only a What I am getting at is why do we need a new keyword, when we can just make Also as somebody mentioned earlier the use of a trampoline and the fact that a tail call optimization is to essentially replace a recursive I thought the whole rationality behind the adding of new features in Go was that we should do our best to avoid feature overlap when it makes sense. I mean, from what I've seen people are suggesting is essentially.
Which WorkA uses the same stack frame when it recursively calls itself and WorkB doesn't. How is this any different?
|
If the go has something as |
@ianlancetaylor the |
@egorse Sorry, I misunderstood your suggestion. I don't see how to implement removing stack frames from an active stack. Even if we did implement it, it seems like it could have very strange behavior. |
@ianlancetaylor defer/recover/panic are capable to alter pc and sp without problem. hostile code can lead to strange behaviour but as well unsafe is there to shoot in the foot. |
Maybe I'm misunderstanding what you meant, but that seems a lot like saying that unrestricted |
@DeedleFake here is what I mean:
|
What happens to the stack when you call
This seems extremely error-prone, and it also doesn't seem like it would work for several of the use cases of |
@DeedleFake I did not look yet how the defer/recover/panic unwinds the stack. probably there are no stack allocated variables and those all escape to heap. but if those triplets works - then it definitely possible safely remove frames from stack. I think the top frame belongs to currently running function (otherway where the local vars are allocated?). So the Lets for a while ignore difficulties with return values.
or via
|
I don't see what the benefit of this over |
@DeedleFake this was just funny generalisation of what might be needed for others cases. Meanwhile The reflect is not soo bad. reflect.TypeOf already does not lead to call. ValueOf lead to call but that's not big deal (if check code) and probably there is case on improvements. |
A.S. failed to convince B.S. to make templates more like Ada generics. A.S. later went on to design and implement "The STL". Restraint and conservatism won out, the more favourable solution being implemented. |
@forskning, please keep the discussion on-topic. It's not at all clear to me what the design of the C++ STL has to do with tail recursion. |
I'm actually just going to freeze this for now. We've adequately covered the various viewpoints in the many comments so far. |
We should only make changes to the language that address an important issue for many people. While this idea is interesting, it doesn't address a significant problem that people have using the language. So we are not going to do it. |
The current Go language does not support tail calls, because they affect
runtime.Callers
and stack backtraces and the like. Becauseruntime.Callers
in particular must be predictable for Go programmers, adding tail calls to the language would require a precise specification of exactly when they do and do not occur. Since people may sometimes not want them, there would have to be a way to avoid a tail call that would otherwise occur.That said, people do want tail calls: #15304, #16798. The Alef language had a
become
statement, documented as follows (from http://doc.cat-v.org/plan_9/2nd_edition/papers/alef/ref):This proposal is to implement a
become
statement with these semantics in Go. The definition would be slightly changed to permit either a list of expressions or a single function call.The text was updated successfully, but these errors were encountered: