124 lines
4.8 KiB
Go
124 lines
4.8 KiB
Go
// Package result provides a generic Expect[T] type that supports three error
|
|
// handling styles without forcing any one of them.
|
|
//
|
|
// # Three modes, one type
|
|
//
|
|
// Expect[T] is a drop-in replacement for (T, error) that also enables
|
|
// panic-based happy-path propagation, and composition of fallible steps into a
|
|
// single expression, when either suits the code better. All three styles
|
|
// compose freely — the same Expect[T] value works in any of them.
|
|
//
|
|
// func parseHost(s string) result.Expect[string] {
|
|
// if s == "" {
|
|
// return result.Failf[string]("host must not be empty")
|
|
// }
|
|
// return result.Ok(s)
|
|
// }
|
|
//
|
|
// Mode 1 — standard Go style (if err != nil):
|
|
//
|
|
// host, err := parseHost(s).Unwrap()
|
|
// if err != nil {
|
|
// return 0, err
|
|
// }
|
|
//
|
|
// Or check and access separately, just as with (T, error):
|
|
//
|
|
// r := parseHost(s)
|
|
// if r.Err() != nil {
|
|
// return 0, r.Err()
|
|
// }
|
|
// use(r.Value())
|
|
//
|
|
// Mode 2 — happy-path style (panic-based propagation):
|
|
//
|
|
// port := parseHost(s).Expect("parse host") // panics on failure
|
|
//
|
|
// Failures are collected at the entry point by [Go] or [Run] and returned as a
|
|
// normal Go error — no goroutine leaks, no silent swallowing.
|
|
//
|
|
// Mode 3 — flow-control style (combinators):
|
|
//
|
|
// port := parsePort(raw).
|
|
// Filter(unprivileged, "privileged port").
|
|
// Or(fromConfigFile).
|
|
// UnwrapOr(8080)
|
|
//
|
|
// Every combinator takes an Expect and returns one, so a computation that can
|
|
// fail at several steps reads as a single expression with no `if err != nil`
|
|
// between them. Error handling is not written; it emerges from the operations
|
|
// chosen. A failure skips every step that follows — no closure runs, no zero
|
|
// value is carried onward as if it were real — until something explicitly
|
|
// handles it ([Expect.Or], [Expect.OrElse], [Expect.UnwrapOr],
|
|
// [Expect.UnwrapOrElse]) or the boundary collects it.
|
|
//
|
|
// Readability is the entire point. Name a step and declare it before the chain
|
|
// when it is reused, or when its body would otherwise bury the pipeline in the
|
|
// middle of the call — the name is then the documentation, and the call site
|
|
// reads as a sentence. A one-shot closure with a single call site stays where it
|
|
// is used: hoisting it out only forces the reader to jump elsewhere to find out
|
|
// what the chain actually does.
|
|
//
|
|
// See flow.go for the full set. Combinators never exit the goroutine, so unlike
|
|
// [Expect.Expect] and [Expect.Must] they are safe inside pkg/ library code.
|
|
//
|
|
// Because Go has no type parameters on methods, a combinator can only be a
|
|
// method when it preserves T: [Expect.Map] and [Expect.AndThen] are the
|
|
// same-type forms, and the package-level [Map] and [AndThen] are the
|
|
// type-changing ones. A chain breaks at the crossing and resumes on the result.
|
|
//
|
|
// # Layering rule
|
|
//
|
|
// Reusable library code (packages under pkg/) must only *return* Expect[T] —
|
|
// it must never call .Expect(), .Must(), or .Expectf() itself. Those methods
|
|
// exit the current goroutine and are only safe inside a goroutine controlled
|
|
// by [Go] or [Run].
|
|
//
|
|
// The right split:
|
|
//
|
|
// - pkg/ functions: return Expect[T] — let the caller decide how to handle it.
|
|
// - Application code (cmd/, HTTP handlers, …): chain .Expect() calls freely,
|
|
// protected by a defer result.Catch(&err) or a result.Run wrapper.
|
|
//
|
|
// Stack traces are captured at the failure site and can be retrieved from the
|
|
// collected error via [StackTrace].
|
|
//
|
|
// # Constructors
|
|
//
|
|
// [Ok] and [Err] are the two field constructors (a success value or a bare
|
|
// error). On top of them, [Failf] originates a failure from a message (embed a
|
|
// cause with %w), [Wrap] propagates an already-failed result into a new type,
|
|
// and [Of] bridges existing (value, error) return signatures:
|
|
//
|
|
// data := result.Of(os.ReadFile("cfg.json")).Expect("read config")
|
|
//
|
|
// # Boundary pattern
|
|
//
|
|
// func run() error {
|
|
// return result.Run(func() {
|
|
// host := parseHost(cfg.Host).Expect("load config host")
|
|
// _ = host // happy path continues …
|
|
// })
|
|
// }
|
|
//
|
|
// [Go] is the typed variant — it returns Expect[T] when the closure produces
|
|
// a value. [Run] is a convenience wrapper for closures that return nothing.
|
|
//
|
|
// [Catch] is an alternative boundary for use with named error returns:
|
|
//
|
|
// func load() (err error) {
|
|
// defer result.Catch(&err)
|
|
// host := parseHost(cfg.Host).Expect("load config host")
|
|
// _ = host
|
|
// return
|
|
// }
|
|
//
|
|
// Important: [Catch] relies on recover() and only works with the default
|
|
// (panic) build. With -tags result_goexit, Expect and Expectf exit via
|
|
// runtime.Goexit which recover() cannot intercept — use [Run] or [Go] instead,
|
|
// as they work correctly in both builds.
|
|
//
|
|
// Genuine runtime panics (nil-pointer dereferences, index out of bounds, etc.)
|
|
// are not recovered — they still crash the program, as they should.
|
|
package result
|