Description
Background
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.
Proposal
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.
Use-cases
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:
- avoid repetition of checks on when to break/return
- avoid repetition of error handling code
- ranging over a channel
In the example below we will look at the specific use-case of avoiding repeating error handling, and for detecting a timeout.
Example Go1 code to improve
The constructed code below does several different checks at a periodic basis:
- Is there an error?
- Has the deadline expired so that we should abort?
- Is the result from the last step satisfactory so that we can return?
// 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:
- using closures -- something which may be OK for long functions, but perhaps to verbose for short ones
- by flipping the if check so that the happy-path is evaluated inside the if clause, and the error handling is done once at the bottom. This stills require either repeating the check, or to use closures.
Plausible new syntax
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.
Break-step syntax example
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
}
for-loop semantics example
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)
}
if / repeat syntax example
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
}
Proposal updates
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.
Activity
mikeschinkel commentedon Aug 18, 2018
@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?smyrman commentedon Aug 18, 2018
@mikeschinkel, yes, an explicit point for where to reevaluate the condition is the point of
step
.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:
mikeschinkel commentedon Aug 18, 2018
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?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.smyrman commentedon Aug 18, 2018
When does
time.Now()
change?mikeschinkel commentedon Aug 18, 2018
@smyrman Give me a full example in how you would use it and I'll answer.
smyrman commentedon Aug 18, 2018
The general answer is every time it's run. The
deadline
in my example above is static, yet it expires because the result oftime.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.
smyrman commentedon Aug 18, 2018
I do think you have helped me come up with more use-cases where this statement could potentially be useful, other than error handling:
networkimprov commentedon Aug 18, 2018
I think you want
break if Condition
However I much prefer the collect/try construct which you've branched this from.
mikeschinkel commentedon Aug 18, 2018
@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 afunc
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.
mikeschinkel commentedon Aug 18, 2018
@networkimprov Would you mind elaborating as to why you like the collect/try construct better?
jharshman commentedon Aug 18, 2018
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 abreak if cond
added to the syntax other than make it kinda hard to read.Furthermore, if
step
isn't implemented then why do abreak if cond
? It's essentially a different method of checking return values after function invocation which to me doesn't flow very well.networkimprov commentedon Aug 18, 2018
I suppose the separate lines with
step
annoy me. Maybe this tho...Also I want a way to flag fatal errors and indicate a fatal error handler...
20 remaining items