golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.2k stars 17.57k forks source link

proposal: spec: error handling via iterator-inspired handler functions #69734

Open DeedleFake opened 1 hour ago

DeedleFake commented 1 hour ago

Go Programming Experience

Experienced

Other Languages Experience

Elixir, JavaScript, Ruby, Kotlin, Dart, Python, C

Related Idea

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

This idea involves several ideas, including handler functions and guard clauses, but it goes about it quite differently.

Does this affect error handling?

Many previous error handling proposals had issues with return values other than errors and the semantics around how handler functions were called and scoped. This attempts to work around those problems by instead using a similar higher-order function approach to what range-over-func uses.

Is this about generics?

No.

Proposal

With the range-over-func, Go, for the first time, has functionality in which calling a function can directly cause another function to return. The compiler generates a function from the body of the loop, but that function has special semantics around returns which will cause the function containing the loop to return when a return is executed inside of the loop body.

Despite this, the functionality is not particularly useful for error handling, as it is not only unweildy, but it actually often pollutes the code more than the standard if err != nil does:

func handle(err error) iter.Seq[error] {
  return func(yield func(err) bool) {
    if err != nil {
      yield(fmt.Errorf("parse int: %w", err)
    }
  }
}

func Example() (int, error) {
  v1, err := strconv.ParseInt(str1, 10, 0)
  for err := range handle(err) {
    return 0, err
  }

  v2, err := strconv.ParseInt(str2, 10, 0)
  for err := range handle(err) {
    return 0, err
  }

  return int(v1 + v2), nil
}

Proposal

I propose adding a concept of guard functions which are invoked via an operator (?, perhaps, but I'm not so stuck on this. I also thought of ||.) after a function call. These functions would take as arguments the return types of the function as well as a special function created automatically by the compiler. This extra function argument would take as arguments the returns types of the function in which the guard clause was used and, when called, would cause the function to return with those arguments. The guard function would return the same types as those it was called with, minus the extra function.

That explanation is a bit obtuse, so here's an example:

func handle[R, V any](ret func(R, error), v V, err error) V {
  if err != nil {
    var r R
    ret(r, err)
  }

  return v, err
}

func Example() (int, error) {
  v1 := strconv.ParseInt(str1, 10, 0) ? handle
  v2 := strconv.ParseInt(str2, 10, 0) ? handle
  return int(v1 + v2), nil
}

In the example, the ret argument to handle() is a special function created by the compiler. When handle() calls it, it causes Example() to return with the values that it was given. The arguments after the first to handle() are the return values of strconv.ParseInt(). The entire f() ? handle expression returns the return value of handle() as long as ret is never called.

I believe that this approach solves a number of problems with previous proposals. For one thing, it fits cleanly with generics to allow for crafting custom handler functions that work across a range of types. It also isn't error-specific: Handler functions could be written with any custom conditions on calling ret that they want to.

The syntax is not something that I'm stuck on, as mentioned previously. I think it should be operator-based just to avoid potential problems with new keywords, but the primary idea here is the special ret function, not the syntax. An alternative syntax could even be to put the guard function before the function being called, i.e. v := handle?strconv.ParseInt(str, 10, 0) or something.

Language Spec Changes

The section on function calls would be changed to mention guard clauses and guard functions, and these would then also be given their own section explaining their semantics.

Informal Change

Guard functions are regular functions designed to be called via a guard clause which are passed, along with other things, a special function that can be used to cause the calling function to return.

Is this change backward compatible?

Yes.

Orthogonality: How does this change interact or overlap with existing features?

I think it fits well with generics and the existing error handling mechanisms.

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

Harder. People got surprisingly confused over how the new iterator functions work, although they seem to have gotten used to them pretty quickly, but I expect that the reaction to this would be pretty similar due simply to its higher-order function based design.

Cost Description

Slightly more complexity in the language's syntax.

Changes to Go ToolChain

Anything that parses Go code would be affected.

Performance Costs

Likely a very small compile-time penalty, but probably essentially no run-time penalty with proper optimization.

Prototype

No response

gabyhelp commented 1 hour ago

Related Issues and Documentation

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)