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: error handling by template functions #57822

Closed
gohryt opened this issue Jan 16, 2023 · 23 comments
Closed

proposal: Go 2: error handling by template functions #57822

gohryt opened this issue Jan 16, 2023 · 23 comments
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Proposal Proposal-FinalCommentPeriod v2 A language change or incompatible library change
Milestone

Comments

@gohryt
Copy link

gohryt commented Jan 16, 2023

Author

Consider myself as intermediate go developer.
Experience: 2 years in production with go.
Other language experience: the most experience is in Js, tried many interesting things from LLVM IR to Nim and Odin.

Related proposals

Has this been proposed before? Variations have been proposed, this is discussed in the proposal.
Affect error handling: yes

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

This will make go harder by one middle-sized manual page. This will not be mandatory-to-use.

Proposal

Updated by comments at 19.01.23

Add a new "function" type template, special expression type for code blocks and inline function for expression insertion to template. As example

template orPanic(err error, log expression) {
    if err != nil {
        inline(log)
        panic(err)
    }
}

func main() {
    file, err := os.Open(filename)
    orPanic(err, { log.Println(err) })
    
    _, err = file.WriteString("example string")
    orPanic(err, { log.Println(err) })
    
    fmt.Println("string wrote")
}

should work as

func main() {
    file, err := os.Open(filename)
    if err != nil {
        log.Println(err)
        panic(err)
    }
    _, err = file.WriteString("example string")
    if err != nil {
        log.Println(err)
        panic(err)
    }
    println("string wrote")
}

The most important difference between func and template is that func share variables with template, and template is only a template of actions to insert to func.

It seems like universal try should be added to builtin library

template try(err error, catch expression) {
    if err != nil {
        inline(catch) 
    }
}

Why not use special keywords like try, catch and others?

The first reason is my personal reason - I wrote this sentence after monthly reading of other proposals on this topic which make me unhappy.
I selected Go as my primary language because of it's simplicity and human readability and i think new keywords will make it Java.

The second reason is that error handling problem is not error handling problem at all, it's a problem of template code which enrage developers. Now it's about errors, next we'll find some old-new places needs in template code problem solving.

Backward compatibility:

Full because no any go program in world use template keyword.

Example code:

func OnRequest(response http.ResponseWriter, request *http.Request) {  
    onError := template(err error, status int) {
        if err != nil {
            response.WriteHeader(status)
            return
        }
    }

    ...

    onError(json.NewDecoder(req.Body).Decode(...), http.StatusBadRequest)

    ...
}

should work as

func OnRequest(response http.ResponseWriter, request *http.Request) { 
    ...

    err := json.NewDecoder(req.Body).Decode(...)
    if err != nil {
        response.WriteHeader(http.StatusBadRequest)
        return
    }

    ...
}
func main() {
    _, err := os.Open(...)
    try(err, { return }) // universal try from builtin example (above) usage
}
func ShouldReturn() (*Example, err) {
    err := ...
    try(err, { return nil, err }) // universal try from builtin example (above) usage
}

should work as

func main() {
    _, err := os.Open(...)
    if err != nil {
        return
    }
}
func ShouldReturn() (*Example, err) {
    err := ...
    if err != nil {
        return nil, err
    }
}

Cost of this proposal:

Tools to rework: compiler, vet, gopls
Compile time cost: grows with the size of the project, seems like generics cost
Runtime cost: no cost

Possible implementation:

The most stupid is read code for template and insert it on the next read, but it's not a go way.
I tried to write prototype as preprocessor but didn't finish it.

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

The most important difference between func and inline is that func share variables with inline,

Can you clarify what you mean by "share variables"? Thanks.

@ianlancetaylor
Copy link
Contributor

It seems confusing that return in an inline function doesn't return from that function, it effectively returns from that function's caller. In that sense there are some similarities to #54361. Another way to say that is that although the syntax of inline is much like the syntax of func, it means something very different.

What happens if an inline function calls itself recursively?

@gohryt
Copy link
Author

gohryt commented Jan 16, 2023

Inline is not a function, it's a code that will be inserted to function. With this behavior return not returns us from inline function, it'll be simply written to the function wich contains call to this inline.
Here I should probably say that I was choosing between two names inline and template, but inline seems more understandable.

By variables sharing i meant that since inline is just text that will be inserted into the function, there should be no stack allocation, copying variables and other complex things that happen when calling the function.
This should just be inserted into the function text .
In fact, the inline or template variables are just fillers that will be replaced with the variables specified in the inline call.

@seankhliao
Copy link
Member

see also #32473 for another take on nonlocal returns

this looks like a more specific example of #35093 ?

@gohryt
Copy link
Author

gohryt commented Jan 16, 2023

#32473 and #35093 are very different approaches to my. It's more about return leveling that is very interesting, but not situable for error handling.
I think call function on each check if err != nil is a bad idea. And all its suggestions has a very unfriendly syntax for me.
inline has no level, it's not a function, it's only code template to place on it's calls.

@ianlancetaylor
Copy link
Contributor

This sounds like #32620.

@gohryt
Copy link
Author

gohryt commented Jan 17, 2023

Yeah, it's very close, I didn't see it. The only thing to think about - compilation scripting language is harder to implement and its syntax is a very hard question.

@Pat3ickI
Copy link

This may work but when it comes to dealing with interface as a parameter how will go:inline or inline Keyword work especially when it comes to switch statements?

@gohryt
Copy link
Author

gohryt commented Jan 17, 2023

I didn't really understand what the problem with interfaces is?
When we declaring inline, argument types are fixed as with function arguments.

inline Read(reader io.Reader, to []byte) {
    ..., err := reader.Read(to)
}
func main() {
    Read(os.Stdout, make([]byte, 16))
}

should be "translated" to

func main() {
    t1 = io.Reader(os.Stdin)
    ..., err := t1.Read(make([]byte, 16))
}

or

inline ToString(source any) (target string, err error) {
    switch source.(type) {
    case string:
        target = source.(string)
    case int:
        target = strconv.Itoa(source.(int))
    default:
        err = errors.New("ToString: unknown type")
    }
} 
func main() {
    str, err := ToString(2023)
    fmt.Println(str, err)
}

to

func main() {
    t1 := any(2023)
    str := ""
    err := error(nil)

    switch t1.(type) {
    case string:
        str = t1.(string)
    case int:
        str = strconv.Itoa(t1.(int))
    default:
        err = errors.New("ToString: unknown type")
    }
    fmt.Println(str, err)
}

@Pat3ickI
Copy link

if the inline Keyword is just a template for code replacement at compile time, it would be better to clarify the key differences between func and inline like inline can only receive values of the same type in the given parameters

inline IsString(T any) {
    switch T.(type) {
    case string: 
        fmt.Println(“This parameter is a string)
    default: 
        fmt.Println(“this is not a string)
}

// compile time error because 123 is not of type any
func main() {
    IsString(123)
}

Or inline Does not support the return keyword like
inline IsString(T any) return T
But when there’s return in the inline block every other code written after when a function call it, it will be a compile time error

inline IsString(T string) {
    return T+ “T string”
}

func main() {
    var str string = “string”
    IsString(str)
    fmt.Println(str) // error 
}

// but this can be permitted if it’s in if, switch like blocks and 
// it must align with the returned type of the function 

inline IsString(T string) {
    if T == “string” {
        return “string1”
    }
}

func stringT() string {
    str := “string”
    IsString(str)
    return “str”
}

I think if there’s no keyword return for inline It can increase confusion and complexity so return keyword should be added for the inline
Also what happens if there’s inline Str and func Str will the compiler reject duplicate naming or find a smart way to know which is which ?

@gohryt
Copy link
Author

gohryt commented Jan 17, 2023

Returning from inline is case i thinked a long time.

My conclusion was that u can't return from inline, because it's only template to write to real code.

The only case in which require return is if we decide that template should be programmable as in #32620

Type checking is a good idea but

inline IsString(T any) {
    switch T.(type) {
    case string: 
        fmt.Println(“This parameter is a string)
    default: 
        fmt.Println(“this is not a string)
}
func main() {
    IsString(123)
}

should works because Go func automatically converts its arguments to any. This should converts to

inline IsString(T any) {
    switch T.(type) {
    case string: 
        fmt.Println(“This parameter is a string)
    default: 
        fmt.Println(“this is not a string)
}
func main() {
    t1 := any(123)
    switch t1.(type) {
    case string: 
        fmt.Println(“This parameter is a string)
    default: 
        fmt.Println(“this is not a string)
}

The last is parsing question and it seems like the simpliest because of Go code rules: if you can't have var Str and func Str in one package you also can't have inline Str and func Str.

@Pat3ickI
Copy link

This can be a problem because most of error handling complains always focused on

if err != nil {
    return err
}

so not allowing return in inline defeats the purpose for like 60 - 70% error handling issues some like #54361 may not be possible, if it’s like

if err != nil {
    panic(err)
}

the compiler is smart enough to inline it but as it comes to a more sophisticated error handling like with switch statements inlining can’t be possible (last I recall), this can be useful in solving the if err != nil { return err } but how will inline be regarded in use cases with return, For the cost some developers can abuse this because it can eliminate function call overhead but also it can also increase binary size, to me it’s seems like a fair trade. solving the relationship between return and inline can be better

@gohryt
Copy link
Author

gohryt commented Jan 18, 2023

It's a good question, i didn't think about it because i personally like named returns and return for me is just return without err .

U also can write something like this i think

func ShouldReturn() (*Example, err) {
    try := inline try(err error) {
        if err != nil {
            return nil, err
        }
    } 

    err := ...
    try(err)
}

as

func ShouldReturn() (*Example, err) {
    err := ...
    if err != nil {
        return nil, err
    }
}

The only limitation that in this case u can't write universal try.

While trying to solve this problem, it seems to me that we will again come to programmable compilation.
It may be something like

template try(err error, catch expression) {
    if err != nil {
        inline(catch) 
    }
}

func main() {
    _, err := os.Open(...)
    try(err, { return })
}

func ShouldReturn() (*Example, err) {
    err := ...
    try(err, { return nil, err })
}

It's still easier than classical try catch finally with optional guard and others, but i thing this will be hard to implement in compiler. It's like a new quality stage.

At least in my experimental preprocessor, which I'm going to publish soon to test this concept, it seems very difficult to implement.

@gohryt gohryt changed the title proposal: Go 2: error handling by inline functions proposal: Go 2: error handling by template functions Jan 19, 2023
@beoran
Copy link

beoran commented Jan 20, 2023

I proposed before that we could add hygienic macros to Go , but this was rejected. #32620 How is this proposal different?

@gohryt
Copy link
Author

gohryt commented Jan 20, 2023

@beoran Some syntax difference and my example preprocessor is on the way

@xiaokentrl
Copy link

//Hand Error

func main() {  

    f, err := os.Open("/test.txt") :e {
            fmt.Println( e )
    }

    //or
    f, err := os.Open("/test.txt") :e HandEorr( e )

    //or
    f, err := os.Open("/test.txt") e: {
        fmt.Println(e)
    }

    f, err := os.Open("/test.txt") e:

}

@seankhliao
Copy link
Member

@xiaokentrl that's just #37243 #41908 #56895 with a different token

@Pat3ickI
Copy link

Pat3ickI commented Jan 21, 2023

Why not create an extension to go then support all these features that are typed related like go generate, having a file extension like examples.ext.go Then just transpile directly to go code like Gopherjs instead of Js.Get(…) it’ll alot smoother because the basically have the same type system, am pretty sure they’ll reject it asap lol OR we combine type format of Cgo with the characteristics embed ?, you allow certain features to be imported then used it as a comment then the compiler will notice it, like

package main 

import (
    “type/inline” // or any name 
     “fmt”
)

// the ‘-‘ is then used to define the template or so, 
// or ditch it entirely and use the  type name as a reference

// go:inline -try   
// inline (err error) error {
//    if err != nil {
//        return err
//    }
//}

// then create a function or something that directs to `go:inline -try`

// go:inline try
type try func(err error) error

func aFunction() error {
    T, err := DoSomething()
    try(err)
    fmt.Println(T)
}
// It then Translates to 

func aFunction() error {
    T, err := DoSomething() 
     if err != nil {
        return err
     }
}

if a developer wants to return two or more then

//go:inline -try 
// inline (err error, t T) (T, error) {
//    if err != nil { 
//        return t, err 
//}

//go:inline try
type try func(err error, t T) (T, error)  


func aFunction() (T, error) {
    T, err := DoSomething()
    try(err, T)
}

the //go:inline try can be removed where the type name can be used

//go:inline
type try func(err error) error

there’re also some rules that I applied above about inline keyword:

  • the package offers no logic or type conversion to what type given must be inline with the params and both the returned types and order
  • when return is called in the first block every other function that called it must be returned else compile time error
// go:inline try
// inline (ind int) int {
//    return ind+58
//}

func aFunction() int {
  intd := DoSomething()
  do(intd)
  return 1 // error 
}

  • This can be possible only if the developer import the specific package
  • Generics is not supported

with this is reduces the load of verbose and complex type system while giving features when it’s needed by the developer,
There’re some faults like it can increase the compile time, been harder to maintain tools like gopls or the type checker as a whole, and it gives a complex way for just if err != nil { return err } but it has alot of uses and not just dedicating a certain number of new keywords just for error handling. The api names can be something suitable (am not good at naming apis), the type try func(err error) error can be exported just the usual first capital letter

@gohryt
Copy link
Author

gohryt commented Jan 21, 2023

@Patrickmitech I'm in process of preprocessor creation. Seem's like it will be available in two or three days.
I believe that this way of template code problem solving can be great for most developers, but as first I should end it and then we should test it.

@xiaokentrl

This comment was marked as spam.

@gohryt
Copy link
Author

gohryt commented Jan 25, 2023

Little update: my example preprocessor is in the process of writing, I need a little more time for correct import work. I'll update the first post when it'll be ready.

@ianlancetaylor
Copy link
Contributor

These template functions are very unlike anything else we have in Go. They introduce a new macro concept. It's an idea one could pursue, but it's complex, and would have to be fully explored independently of any connection to error handling. We would have to understand the name scoping, blocks as arguments, and other ideas that do not exist in Go today.

This is not a direction we're going to take with Go.

Therefore this is a likely decline. Leaving open for four weeks for final comments.

@ianlancetaylor
Copy link
Contributor

No further comments.

@ianlancetaylor ianlancetaylor closed this as not planned Won't fix, can't repro, duplicate, stale Mar 1, 2023
@golang golang locked and limited conversation to collaborators Feb 29, 2024
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 Proposal-FinalCommentPeriod v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

7 participants