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: Leaf Operator with "throw" keyword for error handling #51415

Closed
ghost opened this issue Mar 1, 2022 · 9 comments
Closed

proposal: Go 2: Leaf Operator with "throw" keyword for error handling #51415

ghost opened this issue Mar 1, 2022 · 9 comments
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@ghost
Copy link

ghost commented Mar 1, 2022

Author background

  • Would you consider yourself a novice, intermediate, or experienced Go programmer?

    Novice programmer on Go

  • What other languages do you have experience with?

    Java, Javascript, Python, PL/SQL, Pascal

Related proposals

  • Has this idea, or one like it, been proposed before?

    No

    • If so, how does this proposal differ?

      N/A

  • Does this affect error handling?

    Yes

    • If so, how does this differ from previous error handling proposals?

      This proposal adds the leaf concept as new operator and add a new reserved keyword for error handling. On the future this concept could be used for new behaviours to do other things like validations for example.

      Other proposals

      Based on the different proposals, I don't see enought flexibility to add custom function return types, and creating new code blocks like try, catch, etc. keeps limited the possibility to handle different nested errors because you need to catch everything and the catch concept is a design problem right now in Java and other languages as well.

      Also combinations of different symbols ! # ^ sounds good and others looks complex (skipping the cryptic part) but is not easy to define a custom return type the same happens with must, guard etc.

      Here are the meta document with all proposals language: Go 2: error handling meta issue #40432

  • Is this about generics?

    No

    • If so, how does this relate to the accepted design and other generics proposals?

      N/A

Proposal

  • What is the proposed change?

    Add new unary operator as leaf concept and add a new reserved keyword to propagate errors.

  • Who does this proposal help, and why?

    Helps to developers avoid writing repeated code when propagating errors, also opens the door to add new functionalities on the future based on leaf paths.

  • Please describe as precisely as possible the change to the language.

    Add -> as new unary operator, and throw as reserved keyword.

    Skeleton should be something like:

    fn() -> [reserved keyword] [func|any]

    The operator must have these constraints:

    • operator should be called Leaf operator
    • could be typed only after a function invocation
    • body is the right part of the operator, called Leaf body
    • body should have two defined parts, start and end
    • body should be without braces or any operator or symbol
    • body start needs a reserved keyword (now only "throw" keyword is allowed)
    • body end will be receive the last parameter from function called if has it
    • body end could be a variable name to represent the result of the called function
    • body end could be a function that receives as parameter the result of the called function
    • body end doesn't declares a new variable just receives a value

    The reserved keyword must have these constraints:
    • throw is the keyword
    • typed only in lower case
    • should be used only inside a Leaf operator as body start
    • can't be used on any other declaration
  • What would change in the language spec?

    Add new section as Leaf Path https://go.dev/ref/spec#Declarations_and_scope

    Add new keyword throw https://go.dev/ref/spec#Keywords

    Add new operator -> https://go.dev/ref/spec#Operators_and_punctuation

    Add new section as Leaf Operator https://go.dev/ref/spec#Operators

    Add example how to return an error https://go.dev/ref/spec#Errors

  • Please also describe the change informally, as in a class teaching Go.

    This graph represents the flow of the next example, it is independent of how is handled the error, and I think the proposal solution should be in this line to avoid flow modifications or new code blocks, and must be interpreted by the compiler.

      start                                                end err         end ok
      |                                                    |               |
      setupSatellite                  return error ------> |               |
      |                               |                    calibrate --->  |
      | -> align                      |                    |
              | -> return error ----> |                    |
              | -> return ok ----------------------------> | 
    

    The solution should be take into account the returning type of the function, I think that is the most conditional thing to choose a specific solution.

    This proposal follows the current Golang paradigm avoiding cryptic implementations and using an existent operator like we are using for channels <- but inverted as representing a leaf path (that opens the possibility to add more functionality on the future) and using a new "throw" keyword as indicator that the error will be returned.
    To be consistent I respect all values returned on the function return, but this could improve using only the error.

    The idea is something like is doing on Haskell here is an example https://www.haskellforall.com/2021/05/the-trick-to-avoid-deeply-nested-error.html

    See examples below.

  • Is this change backward compatible?

    Yes, this change is backward compatible

    • Breaking the Go 1 compatibility guarantee is a large cost and requires a large benefit.

    Show example code before and after the change.

    • Before
    func setupSatellite1(lat float64, lng float64) bool, error {
        a, err := align(lat, lng)
        if err != nil {
            return nil, err
        }
        cs, err := getLaunchCodes(lat, lng)
        if err != nil {
            return nil, err
        }
        c := calibrate(a)
        return c, nil
    }
    
    func setupSatellite3(lat float64, lng float64) bool, error {
        onmsg := make(chan string)
        go func() error {
            onmsg <- "ping"
            return fmt.Errorf("bad %s", "ping")
        }()
    
        a, err := <-onmsg
        if err != nil {
            return nil, err
        }
        fmt.Println(a)
        return c, nil
    }
    
    • After
    func setupSatellite1(lat float64, lng float64) bool, error {
        a := align(lat, lng) -> throw err
        cs := getLaunchCodes(lat, lng) -> throw err
        c := calibrate(a)
        return c, nil
    }
    
    func setupSatellite2(lat float64, lng float64) CustomR[bool] {
        a := align(lat, lng) -> throw NewCRErr[bool](err)
        cs := getLaunchCodes(lat, lng) -> throw NewCRErr[bool](err)
        c := calibrate(a)
        return NewCROk(c)
    }
    
    // simple go routine example
    func setupSatellite3(lat float64, lng float64) bool, error {
        onmsg := make(chan string)
        go func() error {
            onmsg <- "ping"
            return fmt.Errorf("bad %s", "ping")
        }() -> throw err
    
        a := <-onmsg
        fmt.Println(a)
        return c, nil
    }
    
    // Example of compilation/linter FAILURES
    func setupSatellite4(lat float64, lng float64) bool, error {
        a := align(lat, lng) -> err             // bad, missing throw, etc.
        a := align(lat, lng) ->                 // bad, missing leaf body
        a, err := align(lat, lng) ->            // bad, missing leaf body
        c := calibrate(a) -> throw nil, err     // bad, fn not returns error
        -> true, nil                            // bad, missing fn before a leaf 
        x := -> true, nil                       // bad, missing fn before a leaf 
        x := align -> throw nil, err            // bad, missing fn before a leaf 
        return c, nil
    }
    
    • Demo helper code, you can ignore this
    type CustomR[T any] {
        ok T
        err error
    }
    
    func NewCRErr[T any](err error) CustomR[T] {
        return CustomR[T]{err: err}
    }
    
    func NewCROk[T any](ok T) CustomR[T] {
        return CustomR[T]{ok: ok}
    }
    
    func align(lat float64, lng float64) int, error {
        if  lat < -90 || lat > 90 {
            return fmt.Errorf("latitude error...%v",lat)
        }
    
        if  lng < -180 || lng > 180 {
            return fmt.Errorf("longitude error...%v",lng)
        }
        return 2222, nil
    }
    
    func getLaunchCodes(lat float64, lng float64) []int, error {
        if  lat == 50.450100 && lng == 30.523399 {
            return []int{22,33,44,55,66}, nil
        }
        return nil, fmt.Errorf("coordinates not allowed %v %v", lat, lng)
    }
    
    func calibrate(lat float64, lng float64) bool {
        return true
    }
    
  • Orthogonality: how does this change interact or overlap with existing features?

    The new operator -> is a leaf concept, defines a flow to do after a function execution, if this operator is found after a function invocation the error will be returned by the leaf body when the throw keyword is present, so this interacts directly after a function invocation.

  • Is the goal of this change a performance improvement?
    No

    • If so, what quantifiable improvement should we expect?
      N/A
    • How would we measure it?
      N/A

Costs

  • Would this change make Go easier or harder to learn, and why?

    Makes Go easier to learn because is a proposal that avoids code repetition and adds a new concept that could be powerful on the future to have more flexibility in one line.

  • What is the cost of this proposal? (Every language change has a cost).

    Modify the compiler, linter tools to interpret the new operator and the new reserved keyword, update docs as well.

  • How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?

    I think vet, gopls, gofmt and maybe I missing some one.

  • What is the compile time cost?

    Should be despreciable at relative terms, but this is subjective right now

  • What is the run time cost?
    Not affected

  • Can you describe a possible implementation?
    I will need help on this

  • Do you have a prototype? (This is not required.)
    No right now

@gopherbot gopherbot added this to the Proposal milestone Mar 1, 2022
@ianlancetaylor ianlancetaylor added error-handling Language & library change proposals that are about error handling. v2 A language change or incompatible library change LanguageChange labels Mar 1, 2022
@ianlancetaylor
Copy link
Contributor

  • I don't see any clear explanation of exactly what throw does.
  • A new keyword is never backward compatible. For example, if throw becomes a keyword, then code like var throw int that works today will start to fail. Why not use an existing keyword, like return?
  • A function call is an expression. Can I write code like f1(f2() -> throw err, f3() -> throw err) -> throw err ?
  • In an expression f() -> throw err the name err looks like an identifier, but it isn't declared anywhere. In Go all identifiers are declared. Where does err come from? What is its type?

@ghost
Copy link
Author

ghost commented Mar 1, 2022

  • I don't see any clear explanation of exactly what throw does.
  • A new keyword is never backward compatible. For example, if throw becomes a keyword, then code like var throw int that works today will start to fail. Why not use an existing keyword, like return?
  • A function call is an expression. Can I write code like f1(f2() -> throw err, f3() -> throw err) -> throw err ?
  • In an expression f() -> throw err the name err looks like an identifier, but it isn't declared anywhere. In Go all identifiers are declared. Where does err come from? What is its type?

Thanks for reviewing, I will update the proposal, here is a summary before to update it.

  • best explanation of throw or return
  • new keyword could be modified by existent return but ¿this mean that we can't expand the language with more descriptive keywords for new features? throw is more descriptive, maybe could be return for current release 1.x, and a specific keyword as breaking change for 2.x, but I don't know if the concept of breaking change is using on Golang.
  • Expression like this f1(f2() -> return err, f3() -> return err) -> return err per definition could be write as it, but is not the best practice I think, could be a warning on linter.
  • the identifier part is for me the tricky part here, I expecting a new body after operator -> and the identifier comes from the function result I will think how to solve this part, maybe like v := f() for this could be f() -> v and v is an identifier with this rule f() -> return v declares a v as variable but only on scope of the leaf body, feedback is welcome. 🤗

@ianlancetaylor
Copy link
Contributor

I don't know if the concept of breaking change is using on Golang.

In general we don't want breaking changes in Go. See https://go.googlesource.com/proposal/+/refs/heads/master/design/28221-go2-transitions.md .

@egonelbre
Copy link
Contributor

Neither of the goroutine examples (before and after) seems to show proper error handling.

With regards to returning an error upwards the chain, I recommend including wrapping of the error in all of the location. It's relatively common to add information to the errors.

@ghost
Copy link
Author

ghost commented Mar 2, 2022

Neither of the goroutine examples (before and after) seems to show proper error handling.

With regards to returning an error upwards the chain, I recommend including wrapping of the error in all of the location. It's relatively common to add information to the errors.

The error info already comes from the source of error add other description on top of other error reflects the absence of something like stacktrace (without using panic) when errors is well designed is not need it to wrap errors every time, one error description is enough.

Any way on the go routine example I just returning the error but you could call a function as well that is part of the definition so you could call fmt.Errorf to add more information

@ghost
Copy link
Author

ghost commented Mar 2, 2022

Doesn't seems we have a lot of flexibility to add features on language level, could be useful design a new layer for compiler?
A layer accepting something like plugins to extend the language with custom things. This would be useful to add new keywords for example that only understand the plugin and you can choose without breaking the language base.

@egonelbre
Copy link
Contributor

egonelbre commented Mar 2, 2022

Any way on the go routine example I just returning the error but you could call a function as well that is part of the definition so you could call fmt.Errorf to add more information.

The "Before" goroutine example does not compile https://gotipplay.golang.org/p/yD_rihzRN0e. Similarly the "after" approach is not clear where the when the error will be synchronized.

The error info already comes from the source of error add other description on top of other error reflects the absence of something like stacktrace (without using panic) when errors is well designed is not need it to wrap errors every time, one error description is enough.

That is not my experience. Stack traces don't cross goroutine boundaries, which means there's a need to augment the information. Similarly, there is information that is not captured in stacktraces. If a language feature makes it more difficult to include extra information when calling back it creates an incentive to not use them, which can make software harder to debug.

Or in other words, it would be better to include the error wrapping in the examples.

@ghost
Copy link
Author

ghost commented Mar 2, 2022

Any way on the go routine example I just returning the error but you could call a function as well that is part of the definition so you could call fmt.Errorf to add more information.

The "Before" goroutine example does not compile https://gotipplay.golang.org/p/yD_rihzRN0e. Similarly the "after" approach is not clear where the when the error will be synchronized.

The error info already comes from the source of error add other description on top of other error reflects the absence of something like stacktrace (without using panic) when errors is well designed is not need it to wrap errors every time, one error description is enough.

That is not my experience. Stack traces cross goroutine boundaries in which case there's a need to augment the information. Similarly, there is information that is not captured in stacktraces. If a language feature makes it more difficult to include extra information when calling back it creates an incentive to not use them, which can make software harder to debug.

Or in other words, it would be better to include the error wrapping in the examples.

Thanks for your response I understand you point of view sorry I was wrote examples directly on the issue without check if compiled

@ghost ghost closed this as completed Mar 2, 2022
@ghost
Copy link
Author

ghost commented Mar 2, 2022

I've close because after some feedback I think this need to be more elaborated, please feel free to propose something like that if someone has better English writing ✍️

Could be nice the discussion for a new layer that accepts plugins what do you think 🤔?

Thanks a lot to all reviewers!

@golang golang locked and limited conversation to collaborators Mar 2, 2023
This issue was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

3 participants