This proposal was originally suggested as comments on #25626, but is probably different enough to deserve it's own evaluation. Special thanks to @mccolljr, @smasher164 and @mikeschinkel for the inspiration.
There has been many suggestions to help with error handling. This suggestion could also help with error handling, but it's not _limited_ to that use-case. It's a pretty generic syntax extension that may have many use-cases where it could simplify code. It could also be that it's not _worth it_, or that other proposals like collect in #25626, or other ones I have not read, would do more for the language. Still I believe this proposal is worth _exploring_.
The proposal is to add syntax to the Go language to support the cases where you want to _repeat_ the same Condition check for several _different_ StatementLists. This is in opposition to the ForStmt where you want to repeat both the Condition check and the StatementList.
I expect the Go team to be better than me on choosing a syntax, so any syntax listed in this proposal are merely _examples_ of possible ways to implement the semantics above. As of yet, I don't consider any of the syntax suggestions in particular to be part of the proposal; The proposal is to introduce the semantics.
There is a lot of things that is easy to do with a for-loop, that could be useful to apply for a long list of statements as well. Some examples could be:
In the example below we will look at the specific use-case of avoiding repeating error handling, and for detecting a timeout.
The constructed code below does several different checks at a periodic basis:
// If ruturns the first satisfactory result, or an error. If deadline is exceeded, ErrTimeout is returned.
func f(deadline time.Time) (*Result, error) {
r, err := Step1()
if err != nil {
return nil, fmt.Errorf("f failed: %s", err)
} else if deadline.After(time.Now()) {
return nil, ErrTimeout
} else if r.Satisfctory() {
return r, nil
}
r, err = Step2(r, 3.4)
if err != nil {
return nil, fmt.Errorf("f failed: %s", err)
} else if deadline.After(time.Now()) {
return nil, ErrTimeout
} else if r.Satisfctory() {
return r, nil
}
r, _ = Step3(r) // ignore errors here as any error will be corrected by Step 4
r, err = Step4(r)
if err != nil {
return nil, fmt.Errorf("f failed: %s", err)
} else if deadline.After(time.Now()) {
return nil, ErrTimeout
}
return r, nil
}
The example is constructed, but imagine that Step1-Step4 are not easily compatible with a for-loop. A real use-case that springs to mind, is _network protocoll_ setup routines (that looks way more complex). Some protocols requires a series of hand-shakes and message passing which do not fit well in a for-loop. Generally the results from the first step is always needed in the next step, and it's necessary to somehow check if we should continue or not after each step.
I won't deny there is also defiantly better ways the code example could be written with such restrains in Go 1, including:
There are _many_ plausible syntax variations that would implement the proposal. Here are some examples. Feel free to comment on them, but I don't consider any syntax in particular to be part of the proposal yet. Because the listed syntax proposals _reuse_ existing statements, the complete set of semantics, as in what you can possibly do with it, is also affected by the choice of which statement to expand.
tl;dr: So far, I think _extending the for loop_ would be the most powerful one while @networkimprov's take on the break-step syntax is the most compact one.
Let's first show the one that was _originally_ listed in this proposal that, perhaps somewhat strangely, extends the _break_ statement to do a _conditional break_. the idea is that the block after the conditional break statement continues to run StatementLists until the check is evaluated to _true_. The evaluation is carried out at the end of each step.
Much like the SwitchStmt, this syntax does not really expand what you can express with the language, but it offers a convenient alternative to the IfStmt for a _specific_ set of use-cases.
func f(deadline time.Time) (*Result, error) {
var err error
var r *Result
break err != nil || deadline.After(time.Now()) || r.Satisfactory() {
step:
r, err = Step1()
step:
r, err = Step2(r)
step:
r, _ = Step3() // ignore errors here
r, err = Step4(r)
}
if err != nil {
return nil, fmt.Errorf("f failed: %s", err)
} else if deadline.After(time.Now()) {
return nil, ErrTimeout
}
return r, nil
}
@networkimprov finds the syntax more clear, and it's defiantly less verbose, if step is replaced by a try statement that is placed _before_ each statement that should result in a recalculation once completed:
func f(deadline time.Time) (*Result, error) {
var err error
var r *Result
break err != nil || deadline.After(time.Now() || r.Satisfactory() {
try r, err = Step1()
try r, err = Step2(r)
r, _ = Step3() // ignore errors here
try r, err = Step4(r)
}
if err != nil {
return nil, fmt.Errorf("f failed: %s", err)
} else if deadline.After(time.Now()) {
return nil, ErrTimeout
}
return r, nil
}
A completely different syntax that would implement the proposal, is to extend the for-loop with some sort of re-check statement; a continue statement that continues the next iteration of the loop at the position where it's located. For the sake of the example let's use the syntax continue>, which is expected to read something like "continue here".
func f(deadline time.Time) (*Result, error) {
var err error
var r *Result
for err == nil && deadline.Before(time.Now()) && !r.Satisfactory() {
r, err = Step1()
continue>
r, err = Step2(r)
continue>
r, _ = Step3() // ignore errors here
r, err = Step4(r)
break
}
if err != nil {
return nil, fmt.Errorf("f failed: %s", err)
} else if deadline.After(time.Now()) {
return nil, ErrTimeout
}
return r, nil
}
Because it's extending the _for_ loop, this could let you do more things as well, like ranging over a _channel_ and send them to alternate functions:
func distribute(jobCh <-chan Job) {
for job := range jobCh {
foo(job)
continue>
bar(job)
continue>
foobar(job)
}
Given the channel can be closed, the Go1 syntax for that would need to be:
func distribute(jobCh <-chan Job) {
for {
job, ok := <-jobCh
if !ok {
break
}
foo(job)
job, ok = <-jobCh
if !ok {
break
}
bar(job)
job, ok = <-jobCh
if !ok {
break
}
foobar(job)
}
Yet a different way the proposal could be implemented, is by adding a repeat keyword to the IfStmt, that would, _repeat_ the same check. If one repeat evaluates to false, all subsequent repeats may be skipped.
func f(deadline time.Time) (*Result, error) {
var err error
var r *Result
if err == nil && deadline.Before(time.Now()) && !r.Satisfactory() {
r, err = Step1()
} repeat {
r, err = Step2(r)
} repeat {
r, _ = Step3() // ignore errors here
r, err = Step4(r)
}
if err != nil {
return nil, fmt.Errorf("f failed: %s", err)
} else if deadline.After(time.Now()) {
return nil, ErrTimeout
}
return r, nil
}
UPDATE 2018-08-19: Rewrote the description try to better justify why the semantics may be useful. At the same time I have thrown in a more complicated example than just doing the normal err != nil check, and added several _different examples_ of syntax that could potentially implement the proposal. Preliminary spec for the break-step semantics removed since there is now no longer just one syntax example.
@smyrman Thanks for the ack.
Question about step:? Why are these important? Is it simply to identify when to evaluate?
They seem to add a lot of visual noise. Why not instead just evaluate whenever err is changed?
@mikeschinkel, yes, an explicit point for where to reevaluate the condition is the point of step.
Why not instead just evaluate whenever err is changed?
It might work for the error case, but not sure it would work well for every case as break is suggeted as a _general_ statement. A Condition can be _anything_, so you would need to know about all the variables used in the condition to know if revaluation should be done or not. It could also be that recalculation is expensive, or that you only want to do it at safe intervals.
Here is an example where assignment evaluation would not be enough:
func f(deadline time.Time) (*Result, error) {
var err error
var r *Result
break err != nil || deadline.After(time.Now()) {
step:
r, err = Step1()
step:
r, err = Step2(r)
step:
r, _ = Step3() // let's imagine it's not safe to abort here.
r, err = Step4(r)
step:
return r, nil
}
if err == nil {
return nil, ErrTimeout
}
return nil, fmt.Errorf("f failed: %s", err)
}
_"A Condition can be anything, so you would need to know about all the variables used in the condition to know if revaluation should be done or not."_
Maybe I am missing something, but it seems since the condition is placed in the break expression so the compiler could identify all variables affected and simply revaluate when those variables change?
_"It could also be that recalculation is expensive, or that you only want to do it at safe intervals."_
Those feel like an edge case that should be handled with explicit if statements rather than through a general purpose approach. IOW: _"Make the common use case trivial, and the uncommon use cases possible."_
For me the step: seems like something that the compiler could too easily do for me so I would rather have the compiler do it than burden me with having to do it at the appropriate times myself.
Maybe I am missing something, but it seems since the condition is placed in the break expression so the compiler could identify all variables affected and simply revaluate when those variables change?
When does time.Now() change?
_"When does
time.Now()change?"_
@smyrman Give me a full example in how you would use it and I'll answer.
The general answer is every time it's run. The deadline in my example above is _static_, yet it expires because the result of time.Now() changes.
I don't see a way that the _step_ can be avoided for this syntax. The _collect_ suggestion in #25626 don't need the step keyword because it's collecting only _one_ variable and not allowing a generic condition check. I think this is a trade-off. You can avoid the step keyword with the semantics of the _collect_ statement, or you can have a generic conditional check and not be able to avoid it.
I do think you have helped me come up with more use-cases where this statement could _potentially_ be useful, other than error handling:
func f(deadline time.Time) (*Result, error) {
var err error
var r *Result
bar := pb.StartNew(4) // https://github.com/cheggaaa/pb
break err != nil || deadline.After(time.Now()) || bar.Increment() == 3 {
step:
r, err = Step1()
step:
r, err = Step2(r)
step:
r, _ = Step3()
r, err = Step4(r)
step:
return r, nil
}
if err == nil {
return nil, ErrTimeout
}
return nil, fmt.Errorf("f failed: %s", err)
}
I think you want break if Condition
However I much prefer the collect/try construct which you've branched this from.
@smyrman Sorry, I did not realize your OP included an example. I was multitasking and so was not careful enough.
I think, as you are realizing, what started as a solution for tedious error handling is trying to become a general purpose construct. It might be trying to do too much. But I'll humor you and assume it makes sense to do this route. _(Ironically, even though @networkimprov doesn't like this proposal, his comment helped me conceptualize what you are trying to do.)_
If the break condition includes a variable comparison, it evaluates when the variable changes. If the condition contains a function call it is also evaluates on every assignment, every control structure condition evaluation and upon every return from a func call, but only once if the return is also an assignment or a control structure condition evaluation.
And if the condition is time consuming then you should restructure your code so that you do not use a time consuming condition with this construct. Instead, use an if statement where you want to evaluate it.
@networkimprov Would you mind elaborating as to _why_ you like the collect/try construct better?
Just my two cents so take it as you would like but I think it's an unneeded feature.
That being said, I don't think step actually adds anything if there was a break if cond added to the syntax other than make it kinda hard to read.
Furthermore, if step isn't implemented then why do a break if cond? It's essentially a different method of checking return values after function invocation which to me doesn't flow very well.
I suppose the separate lines with step annoy me. Maybe this tho...
break if err != nil {
try a, err = f1()
b = f2() // can't break
try c, err = f3()
}
Also I want a way to flag fatal errors and indicate a fatal error handler...
I think the syntax could be in many different ways, and is probably not an important part of the proposal. I want to show _a_ syntax so you can relate to the feature. The important part of the proposal is what it says on the top:
The proposal is to add syntax to the Go language to support the cases where you want to repeat the same Condition check for several different StatementLists. This is in opposition to the ForStmt where you want to repeat both the Condition check and the StatementList.
See the Go spec for details, as it does a great job of explaining those statements once you understand the declaration language.
Maybe it would make sense to steel even more from the ForStmt, includingrange support, initial assignment and post "loop" execution.
If the break condition includes a variable comparison, it evaluates when the variable changes. If the condition contains a function call it is also evaluates on every assignment, every control structure condition evaluation and upon every return from a func call, but only once if the return is also an assignment or a control structure condition evaluation.
I feel that is getting pretty complex, but maybe it's implementable. I still feel the collect proposal is more suitable for "automatic" recalculation.
_"I feel that is getting pretty complex, but maybe it's implementable. I still feel the collect proposal is more suitable for "automatic" recalculation."_
Optimizing complexity is part of what a compiler does. So I think it is very doable, unless someone better at compilers than me can point out why not.
This is not merely alternate keywords; try indicates where condition is tested after results are computed. I can't tell where it's tested in the OP.
break if err != nil {
try a, err = f1()
b = f2() // can't break
try c, err = f3()
}
And syntax strongly affects the reactions of your audience, so make it appealing sooner than later.
And syntax strongly affects the reactions of your audience, so make it appealing sooner than later.
I agree, but I am not sure, if the proposal is accepted, that a syntax should be included other than to serve as an example.
Lets talk about the syntax, which I claim is not important. This is a completely different syntax that still implements the same _proposal_ text:
Let's say we added a repeat keyword as an alternative to else in the IfStmt that simply repeats the previous check:
func f(deadline time.Time) (*Result, error) {
var err error
var r *Result
if err == nil && deadline.Before(time.Now()) {
r, err = Step1()
} repeat {
r, err = Step2(r)
} repeat {
r, _ = Step3()
r, err = Step4(r)
} repeat {
return r, nil
}
if err == nil {
return nil, ErrTimeout
}
return nil, fmt.Errorf("f failed: %s", err)
}
This gives a _completely_ different feal. Syntax _matters_ when it's implemented, but before that we need to know if the _semantics_ makes sense, because if they don't, the syntax won't neither.
Be aware that many gophers are adamant about the necessity of 3 lines of ceremony after each function call, and do not agree that it hurts readability. https://github.com/golang/go/issues/22122
I like what you're trying to do, but you have to explain how people are dying (suffering is insufficient) under the current regime if you hope to change it.
And again, it is not clear in the OP when the condition is tested. I tried to clarify that with my use of try above.
Sure, your syntax example is perfectly fine, and woul implement the proposal.
Proposed
func f() (*Result, error) {
var err error
var r *Result
break err != nil {
step:
r, err = Step1()
step:
r, err = Step2(r)
step:
r, _ = Step3() // ignore errors here
r, err = Step4(r)
step:
return r, nil
}
return nil, fmt.Errorf("f failed: %s", err)
}
vs current
func f() (r *Result, err error) {
if r, err := Step1(); err != nil {
return nil, fmt.Errorf("f failed: %s", err)
}
if r, err = Step2(r); err != nil {
return nil, fmt.Errorf("f failed: %s", err)
}
r, _ = Step3(r) // ignore errors here...
if r, err = Step4(r); err != nil {
return nil, fmt.Errorf("f failed: %s", err)
}
return r, nil
}
@cznic, see this for an example of current: https://github.com/golang/go/issues/25626#issuecomment-414090940
Also see my comment above for a better example of proposed.
Did a large update of the description, also included your example syntax @networkimprov.
Also added a new example syntax that extends the _for_-loop by adding a continue> statement as in "continue here". The idea would be to _iterate_ the for loop, but then continue at the location in the code where the continue> statement is places.
This allows for all of the semantics contained int the first syntax example, and more.
Again, all of this seems like adding unnecessary complexity to the language when the same could be achieved with much simpler syntax. I look at the proposed syntax and just them very hard to grok compared with how we can already code today in Go. I'd much rather see existing syntax used with some added semantics and put the burden on the compiler to sort it out rather than burden the developer with all this. #fwiw #jmtcw
Some meandering observations.
One nice thing about this vs many of the error handling proposals is that the checkpoints are clear and explicit.
The semantics of the repeated evaluation isn't fully specified, e.g. around variable scoping and binding. But it seems like the intent is that it operate in a roughly closure-like manner. And indeed, if you were trying to de-sugar this programmatically, one natural way is with closures:
break E {
S1
step:
S2
step:
S3
}
translates to:
var exit bool
f := func() {
exit = E
if exit {
panic(0) // value unimportant
}
}
func() {
defer func() {
if exit { recover() }
}
S1
f()
S2
f()
S3
}()
(This isn't quite perfect, because if Sx panics, the stack trace will be slightly different. But it is close.)
And with the repeat syntax in https://github.com/golang/go/issues/27075#issuecomment-414094785, this translation is even easier: just make the closure return a bool.
There's one thing you can't do in a closure, which is cause the calling function to return and provide its return values. And there's no obvious way to add such a thing, and you wouldn't want to; local control flow is better.
I like those observations @josharian. This started as syntactic sugar for closures, and perhaps that's all this is.
On the same note, isn't switch-case just syntactic sugar for if/else semantics though? Yet I find the type switch one of the really useful features of the go language.
I thing that the syntax suggestion that is furthest away of _just_ providing syntactic sugar, is probably the continue> (continue here) extension to the for-loop, but that's because that suggestion offers _additional_ semantics. I am still not sure if such an extension would increase readability and be tremendously useful, or increase language complexity without any of those benefits.
The draft proposal in https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling.md seams much better suited for error handling than this proposal.
I am closing it.
Thank you for opening this, it is a worthy idea, and should have seen at least as much support as the proposal you branched from.
I am developing a revision of the check/handle concept, the _#id/catch Error Model_ #27519.
Most helpful comment
Be aware that many gophers are adamant about the necessity of 3 lines of ceremony after each function call, and do not agree that it hurts readability. https://github.com/golang/go/issues/22122
I like what you're trying to do, but you have to explain how people are dying (suffering is insufficient) under the current regime if you hope to change it.
And again, it is not clear in the OP when the condition is tested. I tried to clarify that with my use of
tryabove.