Skip to content

proposal: Go 2: add on statement #33266

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

Closed
celicoo opened this issue Jul 24, 2019 · 10 comments
Closed

proposal: Go 2: add on statement #33266

celicoo opened this issue Jul 24, 2019 · 10 comments
Labels
FrozenDueToAge LanguageChange Suggested changes to the Go language Proposal v2 An incompatible library change
Milestone

Comments

@celicoo
Copy link

celicoo commented Jul 24, 2019

I propose the addition of an on statement. Much like the if statement, the block of the on statement executes when the <expression> is evaluated to true:

package main

import (
    "log"
    "os"
)

func main() {
    stat, err := os.Stat("path/to/file.go")
    on err != nil {
      log.Fatal(err)
    }
}

However, unlike the if statement, the block of the on statement executes whenever the <expression> evaluates to true:

package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    var err error
    on err != nil {
      log.Fatal(err)
    }
    _, err = os.Stat("path/to/file.go")
    _, err = os.Open("path/to/file.go")
}

Those two examples are using error handling because it's a controversial topic right now, but having an on statement is more than just avoiding boilerplate code – in essence, the on statement would improve the language expressiveness in my opinion. The example above shows another applicability:

package main

import (
    "fmt"
    "log"
)

type Response struct {
    Close bool
    Error error
    Data  string
}

func get(url string) *Response {
    // Oversimplified code for the sake of example.
    return &Response{
        Close: true,
        Error: nil,
        Data:  "",
    }
}

func main() {
    resp := get("https://github.com/golang/go/issues/33266")

    on resp.Close {
        fmt.Println("data received:", resp.Data)
    }

    on resp.Error != nil {
        log.Fatal(resp.Error)
    }
}

Thanks in advance.

@gopherbot gopherbot added this to the Proposal milestone Jul 24, 2019
@qrpnxz
Copy link

qrpnxz commented Jul 24, 2019

Biggest problem I can see is that I cannot mix this with normal error handling. If I say on err != nil, then I'm not able to handle errors from different functions differently. It becomes an exception.

@celicoo
Copy link
Author

celicoo commented Jul 24, 2019

@qrpnxz In most cases, you won't mix the error handling of functions that return an error and functions that return a struct with an Error method, but unarguably it's an issue with the proposal. A possible solution for it could be the use of channels:

package main

import (
    "fmt"
    "log"
)

type Response struct {
    Close bool
    Error error
    Data  string
}

func get(url string) *Response {
    // Oversimplified code for the sake of example.
    return &Response{
        Close: true,
        Error: nil,
        Data:  "",
    }
}

func main() {
    c := make(chan error)
    on err := <-c; err != nil {
        log.Fatal(err)
    }

    resp := get("https://github.com/golang/go/issues/33266")

    on resp.Close {
        fmt.Println("data received:", resp.Data)
    }
   
    // This should have been c <-resp.Error instead.
    err <-resp.Error
}

@ianlancetaylor ianlancetaylor added v2 An incompatible library change LanguageChange Suggested changes to the Go language labels Jul 25, 2019
@ianlancetaylor
Copy link
Contributor

What does this mean, from the previous comment?

    on err := <-c; err != nil {
        log.Fatal(err)
    }

You said that on runs the block whenever the expression is true. Presumably this on statement does not cause the program to hang even though <-c would block forever at the time the on statement is first seen. But nothing ever writes to c, so when and how is the on statement evaluated?

@celicoo
Copy link
Author

celicoo commented Jul 25, 2019

@ianlancetaylor let me first address what you pointed about nothing ever being writing to c in the example from my previous comment. I made a mistake, the line:

err <-resp.Error

Should've been:

c <-resp.Error

Sorry for confusing that.

Now, my last comment was an attempt to address the issue brought by @qrpnxz, but you're correct, the current implementation of channels would cause the example to block forever, so I would like to withdraw my proposed solution to use channels with the on statement, it merely doesn't make sense.

Back to the initial proposal, the on statement has the following syntax:

OnStmt = "on" Expression Block .

The Expression evaluates as soon as the on is reached and again if the identifiers' value changes. The following should clarify:

func main() {
    var a int
    on a > 10 {
        fmt.Println("a is now 10")
    }
    a = 10
}

The message a is now 10 should be printed.

Finally but not less important, the following example shows how the on statement can be used to address the issue about mixing the error handling of functions that return an error and functions that return a struct with an Error method:

package main

import (
    "fmt"
    "log"
)

type Response struct {
    Close bool
    Error error
    Data  string
}

func get(url string) *Response {
    // Oversimplified code for the sake of example.
    return &Response{
        Close: true,
        Error: nil,
        Data:  "",
    }
}

func main() {
    var err error
    on err != nil {
        log.Fatal(err)
    }

    resp := get("https://github.com/golang/go/issues/33266")

    on resp.Close {
        fmt.Println("data received:", resp.Data)
    }
   
    on resp.Error != nil {
        err = resp.Error
    }
    
    _, err = os.Stat("path/to/file.go")
}

@maj-o
Copy link

maj-o commented Jul 25, 2019

PropertyChanged event-handler - i think this makes applications much harder to understand. When will a function end? When the observed object is removed by gc - otherwise this would not work. In your first example the application ends before log.Fatal has written a single rune, if - and here is a performace problem - not the execution of all other blocks is paused (not threads, goroutines or functions). In this case just until log.Fatal has finished nothing alse may be executed, cause this would trigger other event-handlers. But there can be much more event-handlers, even embedded event-handlers (inlined or in called functions) - and all of this would live until gc cleans this up.

If i combine your first and second example by moving the line

resp := get("https://github.com/golang/go/issues/33266")

to the end of your func main() - i feel no good - this should write "data received..."
If you feel good until this point i have four questions:

  1. What is the expected behavier if your func main() would be func getResp() *Response - the Close would not be set in this func but in the caller itself - and maybe more than once - would we also read "data received" - though execution is no longer in getResp?
package main

import (
    "fmt"
    "log"
)

type Response struct {
    Close bool
    Error error
    Data  string
}

func get(url string) *Response {
    // Oversimplified code for the sake of example.
    return &Response{
        Close: false,
        Error: nil,
        Data:  "",
    }
}

func getResp() *Response {
    resp := get("https://github.com/golang/go/issues/33266")

    on resp.Close {
        fmt.Println("data received:", resp.Data)
    }

    on resp.Error != nil {
        log.Fatal(resp.Error)
    }
}

func main() {
    response := getResp()
    response.Close = true // does this trigger the on resp.Close ?
    response.Close = true // does this also trigger the on resp.Close ?
    response.Close = false // this should just reset the condition to ask the following ..
    response.Close = true // does this trigger the on resp.Close again ?
}

If You say NO, then what if resp would be declared globaly.

  1. Can on-blocks have embedded on-blocks - inlined or in called functions - and what if in your second example the PropertyChanged event handler for resp.Close sets resp.Error und vice versa how can this blocking or endless events be detected?

  2. Can more then one event handler be assigned to one Property - accidentally or not - in the same scope?

Example:

on err != nil {
...
}
...
on err {
...
}
on err == nil {
}
....
on err != nil {
...
}

If err would be global, this event-handlers could be spread across the hole file or even package in any function and got executed somewhen - , - Nobody writes long functions, but if, multiple on blocks with correlated conditions could apear.

  1. I changed log.Fatal to log.Println, cause i want to see all entries, not just the first and for your first example arises another question: If os.Stat failes, will os.Open be executed or does this break the block or exit the function ?
 _, err = os.Stat("path/to/file.go")
 _, err = os.Open("path/to/file.go")

Maybe i am wrong - but either this does not work as expected at all or it hides execution paths, both is not good - just for errors you can use something like this or put the hole error-logging in a separate package - the great benefits are, that errors can be handled directly where they appear and they get also logged:

package main

import (
	"errors"
	"log"
	"os"
)

var errChan chan error

func init() {
	errChan = make(chan error)
	go logErrors(errChan)
}

func main() {
	_, err1 := os.Stat("./nomain.go")
	errChan <- err1

	_, err2 := os.Open("./nomain.go")
	errChan <- err2

	errChan <- errors.New("no error")
}

func logErrors(errChan <-chan error) {
	for {
		errFromChan := <-errChan
		if errFromChan != nil {
			log.Println(errFromChan)
		}
	}
}

This is readable for me and i can see when and what will happen.

@ianlancetaylor
Copy link
Contributor

What should happen for

func F() int {
    var x, y int
    on x != 0 {
         return y
    }
    fmt.Sscan("1 2", &x, &y)
}

When does the on statement trigger on the change to x?

@celicoo
Copy link
Author

celicoo commented Jul 25, 2019

@ianlancetaylor good point. In this case, the returning value from calling the F function would always be 0, because the on statement would be triggered immediately after the value of x changes. One could argue that this could confuse newcomers, but I honestly don't think so. The on keyword reflects how it works.

@ianlancetaylor
Copy link
Contributor

Note that x will be changed by fmt.Sscan, not by F, so if we are supposed to execute the on statement as soon as fmt.Sscan changes x, I don't see how that can be implemented.

@celicoo
Copy link
Author

celicoo commented Jul 26, 2019

@ianlancetaylor you're right. In this case, this proposal shouldn't move forward at all IMO. Feel free to close the issue if you agree.

@maj-o thank you for the feedback, I wish I had time yesterday to reply you.

@ianlancetaylor
Copy link
Contributor

Thanks, I don't see how to make this work, so closing.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge LanguageChange Suggested changes to the Go language Proposal v2 An incompatible library change
Projects
None yet
Development

No branches or pull requests

5 participants