2.9 KiB
result: Concurrency API — open design problem
Current API
Async[T](fn func() T) <-chan Expect[T]
AsyncOf[T](fn func() Expect[T]) <-chan Expect[T]
Bind[T, A](fn func(A) Expect[T], arg A) func() Expect[T]
Typical call site:
hostCh := result.AsyncOf(result.Bind(parseHost, "localhost"))
portCh := result.AsyncOf(func() result.Expect[int] { return parsePort("8080") })
The problem
Every async call requires either an explicit closure or a Bind call. For
single-arg library functions Bind works cleanly; for zero-arg and multi-arg
functions the caller must write a closure, introducing visual noise that does
not add information.
Why result.Async(parsePort("8080")) does not work
parsePort("8080") is an eager call — it evaluates immediately on the calling
goroutine and returns an int. Async expects func() int. The compiler
rejects it. There is no way in Go to pass a call expression as a deferred
computation without wrapping it in a closure.
Options explored
1. AsyncBind / AsyncBind2 — collapse AsyncOf + Bind
func AsyncBind[T, A any](fn func(A) Expect[T], a A) <-chan Expect[T]
func AsyncBind2[T, A, B any](fn func(A, B) Expect[T], a A, b B) <-chan Expect[T]
Reduces call site to result.AsyncBind(parsePort, "8080").
Downside: still need AsyncBind3, etc. for higher arities; Bind itself
becomes redundant.
2. Higher-level combinator — Join2, Join3, …
Hides channels entirely. Runs N computations concurrently, collects results, calls a combiner only on success:
url, err := result.Join2(
result.Bind(parseHost, "localhost"),
result.Bind(parsePort, "8080"),
func(host string, port int) string {
return fmt.Sprintf("http://%s:%d", host, port)
},
).Unwrap()
Upside: no channels, no closures in application code; most declarative.
Downside: loses the "start goroutines, do other work, collect later"
pattern; still needs Join2/Join3/… per arity; heterogeneous type
combinations explode the generic signature.
3. Reflection / any variadic
A single AsyncCall(fn any, args ...any) using reflect.Call.
Downside: no type safety, runtime panics on arity/type mismatch, poor IDE
support. Not worth it.
Root constraint
Go generics have no variadic type parameters. There is no way to write a single type-safe function that accepts an arbitrary function and its arguments. Every approach above hits this wall and works around it by duplicating the function for each arity (N = 1, 2, 3, …).
Recommended direction when revisiting
Decide first which usage pattern dominates:
- "Fire and forget, collect later" — keep channels visible;
AsyncBind/AsyncBind2are the minimal improvement. - "Run N things, combine result" — adopt
Join2/Join3; channels disappear from application code entirely.
A mixed API (both families) is possible but adds surface area. Keep it small.