Skip to content

proposal: iter: SeqError type, Throw function #67924

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
AndrewHarrisSPU opened this issue Jun 10, 2024 · 7 comments
Closed

proposal: iter: SeqError type, Throw function #67924

AndrewHarrisSPU opened this issue Jun 10, 2024 · 7 comments
Labels
Milestone

Comments

@AndrewHarrisSPU
Copy link

Proposal Details

The proposed API is a definition of a SeqError, associated with an iteration or sequencing abnormality, located in the iter package.

type SeqError func() error

func (SeqError) Error() string

func (SeqError) Unwrap() error

func (SeqError) Is(error) bool
var SeqHazard error // the target matched by SeqError.Is (notably, any custom error may define itself to match `SeqHazard`)

SeqError is an enriched error type with the following properties:

  • Context is added to the error string of a contained error.
  • Dynamic inspection with errors.Unwrap and errors.Is is supported.
  • Sequencing errors may be made statically manifest in some circumstances. For example, between iter.Seq2[T, error] and iter.Seq2[T, iter.SeqError], the latter says something usefully more narrow.
  • The error value remains nil-comparable, or may be lazily evaluated.

Additionally, a Throw function synthesizing a SeqError from an existing error is proposed.

func Throw(error) SeqError

The obvious use case is that an iterator may Throw an error as a SeqError where it would otherwise yield an error on an unhappy path. A quick sketch of where an iterator would throw:

func All() iter.Seq2[V, iter.SeqError] {
    ...

    return func(yield func(V, SeqError) bool){
        ...

        // an unhappy path
        if err != nil {            
            // Throw a SeqError, rather than yield an error
            yield(v, iter.Throw(err))
            return
        }

        // happy path
        if !yield(v, nil) {
            return
        }

        ...
    }
}

Implementation would be short:

type SeqError func() error

func (e SeqError) Error() string {
	return "sequencing hazard: " + e().Error()
}

func (e SeqError) Unwrap() error {
	return e()
}

func (e SeqError) Is(target error) bool {
	return target == SeqHazard
}

var SeqHazard error = errors.New("sequencing hazard")

func Throw(err error) SeqError {
	if err == nil {
		return nil
	}
	return SeqError(func() error {
		return err
	})
}

Adopting this proposal would be asking APIs that export fallible iterators to consistently Throw, or otherwise provide something that statically or dynamically matches SeqError, where appropriate. Assuming no convergence on how iteration errors are handled, maybe it's worth asking that fallible iterators define an iteration error in a common way.

@ianlancetaylor ianlancetaylor moved this to Incoming in Proposals Jun 10, 2024
@ianlancetaylor ianlancetaylor added this to the Proposal milestone Jun 10, 2024
@ianlancetaylor
Copy link
Member

Throw doesn't seem like the best name here. In many languages throw raises an exception or otherwise changes the flow of control. That is not true of this Throw.

@AndrewHarrisSPU
Copy link
Author

Throw doesn't seem like the best name here.

The worst name I came up with was SeqFault.

I'm entirely open to a better name - Hazard maybe? - where Throw might be too squisy.

@anonld
Copy link

anonld commented Jun 10, 2024

Putting the Throw function aside, how about proposal: errors: add UnwrapAll?

func UnwrapAll(e error) iter.Seq[error] {
	return func(yield func(error) bool) bool {
loop:
		for {
			if !yield(e) {
				return false
			}
			switch x := e.(type) {
			case interface{ Unwrap() error }:
				e = x.Unwrap()
			case interface{ Unwrap() []error }:
				for _, e = range x.Unwrap() {
					for e = range UnwrapAll(e) {
						if !yield(e) {
							return false
						}
					}
				}
				fallthrough
			default:
				break loop
			}
		}
		return true
	}
}

With UnwrapAll, errors.Is/As can share the control flow code.
By the way, please correct me if the implementation details above get wrong.

Getting back to the Throw function, you might want something like this:

func doAndThrow(is func(error) bool) {
	do := func() error { ... }
	for e := range UnwrapAll(do()) {
		if is(e) {
			panic(e)
		}
	}
}

To recover from the panic, just use defer and recover in the same way as you call the MustXXX family in rexgep and text/template.

@AndrewHarrisSPU As I may have misinterpreted the goal of your proposal, would you mind further clarifying what problem you are trying to solve? If we are addressing different use cases, I will file another proposal then.

@earthboundkid
Copy link
Contributor

I don't understand why it's func() error. ISTM, this is just a concrete error type and could be struct{ err error }.

@AndrewHarrisSPU
Copy link
Author

AndrewHarrisSPU commented Jun 11, 2024

I don't understand why it's func() error. ISTM, this is just a concrete error type and could be struct{ err error }.

Two details seemed to work out better for func() error:

First, a SeqError would be nil-comparable, and have a nil zero value. A pointer-to-struct {error} is another idea, but it's a little unfortunate that missing the pointer-to part breaks if err != nil.

Second, func() error seemed modestly more flexible for a concurrently held iterator to implement. The conversion from func() error to SeqError, or vice-versa, is a little more immediate. E.G.: context.Err's semantics wouldn't be right for every iterator, but Seq[T] to a Seq[T, SeqError] that just yields SeqError(context.Err) has a coherent meaning when lazily evaluated: if the context shuts down, that's observed as a sequencing hazard.

@rsc
Copy link
Contributor

rsc commented Jun 11, 2024

I don't understand why this machinery would be used instead of Seq2[T, error].

@AndrewHarrisSPU
Copy link
Author

I don't understand why this machinery would be used instead of Seq2[T, error].

I tried to formulate some finesse here - I would not to want to coerce the decision against Seq2[T, error] and for Seq2[T, SeqError], but leave it more open-ended. The idea would be, an implementation can opt into exporting the SeqError form, or a user can convert into the SeqError form. If the error form is preferred, there is still some utility wrapping provided by consistently Throwing (and, a fallible iterator really should be able to identify where to Throw).

What should be maintained invariantly is a static inference about errors in Seq2[T, SeqError] - that they say something about a disruption to the overall state of the sequence, and not something non-disruptive to control flow, exclusively about a value of T.

I think that invariant is useful as a hook for readability and analysis, and as a premise for iterator composition. For example, @jba's error function idea could be functionally derived from Seq2[T, SeqError] more confidently than from Seq2[T, error]. Or, I worry that it's possible to lose track of the invariant with xiter-style composition just parameterized by error.

@rsc
Copy link
Contributor

rsc commented Jun 12, 2024

This proposal has been declined as infeasible.
— rsc for the proposal review group

@rsc rsc moved this from Incoming to Declined in Proposals Jun 12, 2024
@rsc rsc closed this as completed Jun 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Declined
Development

No branches or pull requests

5 participants