Skip to content
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 syntax to repeat a Condition check for different StatementLists #27075

Closed
smyrman opened this issue Aug 18, 2018 · 27 comments
Labels
FrozenDueToAge Proposal v2 A language change or incompatible library change
Milestone

Comments

@smyrman
Copy link

smyrman commented Aug 18, 2018

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.

@mikeschinkel
Copy link

@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
Copy link
Author

smyrman commented Aug 18, 2018

@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
Copy link

"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
Copy link
Author

smyrman commented 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?

When does time.Now() change?

@mikeschinkel
Copy link

mikeschinkel commented Aug 18, 2018

"When does time.Now() change?"

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

@smyrman
Copy link
Author

smyrman commented 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 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
Copy link
Author

smyrman commented 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:

  • 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
Copy link

I think you want break if Condition

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

@mikeschinkel
Copy link

mikeschinkel commented 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 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
Copy link

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

@jharshman
Copy link
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
Copy link

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

@smyrman
Copy link
Author

smyrman commented Aug 19, 2018

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.

@mikeschinkel
Copy link

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

@networkimprov
Copy link

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.

@smyrman
Copy link
Author

smyrman commented Aug 19, 2018

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.

@meirf meirf added the v2 A language change or incompatible library change label Aug 19, 2018
@networkimprov
Copy link

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

@smyrman
Copy link
Author

smyrman commented Aug 19, 2018

Sure, your syntax example is perfectly fine, and woul implement the proposal.

@cznic
Copy link
Contributor

cznic commented Aug 19, 2018

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
}

@networkimprov
Copy link

@cznic, see this for an example of current: #25626 (comment)

Also see my comment above for a better example of proposed.

@smyrman smyrman changed the title proposal: Go 2: add break and step semantics to the language proposal: Go 2: add syntax to repeat a Condition check for different StatementLists Aug 19, 2018
@smyrman
Copy link
Author

smyrman commented Aug 19, 2018

Did a large update of the description, also included your example syntax @networkimprov.

@smyrman
Copy link
Author

smyrman commented Aug 19, 2018

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.

@mikeschinkel
Copy link

mikeschinkel commented Aug 19, 2018

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

@josharian
Copy link
Contributor

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 #27075 (comment), 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.

@smyrman
Copy link
Author

smyrman commented Aug 20, 2018

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.

@smyrman
Copy link
Author

smyrman commented Aug 30, 2018

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.

@smyrman smyrman closed this as completed Aug 30, 2018
@networkimprov
Copy link

networkimprov commented Aug 30, 2018

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.

@golang golang locked and limited conversation to collaborators Sep 15, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

8 participants