Skip to content

proposal: Go 2: add syntax to repeat a Condition check for different StatementLists #27075

Closed
@smyrman

Description

@smyrman

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:

  1. Is there an error?
  2. Has the deadline expired so that we should abort?
  3. 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

added this to the Proposal milestone on Aug 18, 2018
mikeschinkel

mikeschinkel commented on Aug 18, 2018

@mikeschinkel

@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

smyrman commented on Aug 18, 2018

@smyrman
Author

@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)
}

mikeschinkel

mikeschinkel commented on Aug 18, 2018

@mikeschinkel

"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.

smyrman

smyrman commented on Aug 18, 2018

@smyrman
Author

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?

mikeschinkel

mikeschinkel commented on Aug 18, 2018

@mikeschinkel

"When does time.Now() change?"

@smyrman Give me a full example in how you would use it and I'll answer.

smyrman

smyrman commented on Aug 18, 2018

@smyrman
Author

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.

smyrman

smyrman commented on Aug 18, 2018

@smyrman
Author

I do think you have helped me come up with more use-cases where this statement could potentially be useful, other than error handling:

  • periodic deadline calculation
  • progress bar updates
  • all of the above
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)
}

networkimprov

networkimprov commented on Aug 18, 2018

@networkimprov

I think you want break if Condition

However I much prefer the collect/try construct which you've branched this from.

mikeschinkel

mikeschinkel commented on Aug 18, 2018

@mikeschinkel

@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.

mikeschinkel

mikeschinkel commented on Aug 18, 2018

@mikeschinkel

@networkimprov Would you mind elaborating as to why you like the collect/try construct better?

jharshman

jharshman commented on Aug 18, 2018

@jharshman
Contributor

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.

networkimprov

networkimprov commented on Aug 18, 2018

@networkimprov

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...

20 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @josharian@smyrman@mikeschinkel@networkimprov@cznic

        Issue actions

          proposal: Go 2: add syntax to repeat a Condition check for different StatementLists · Issue #27075 · golang/go