88 lines
2.9 KiB
Markdown
88 lines
2.9 KiB
Markdown
# result: Concurrency API — open design problem
|
|
|
|
## Current API
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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`
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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` /
|
|
`AsyncBind2` are 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.
|