Motivation
Chaining multiple dependent operations with andThen forces nested closures to keep earlier values in scope:
getUser(id)
.andThen(user =>
getRole(user).andThen(role =>
getPermissions(role).map(perms => ({ user, role, perms }))
)
)
safeTry solves this with generators, but there's demand for a method-chain approach too.
fp-ts, Effect, and byethrow all have Do + bind for this. Same idea as Haskell's do notation, Scala's for comprehension, F#'s Computation Expressions, and Kotlin/Arrow's either { } + .bind().
Proposed API
Given the following functions:
declare function getUser(id: string): Result<User, GetUserError>
declare function getRole(user: User): Result<Role, GetRoleError>
declare function getPermissions(role: Role): Result<Perms, GetPermsError>
Entry point
Result.Do // Result<{}, never>
ResultAsync.Do // ResultAsync<{}, never>
bind: add a value from a Result-returning function
Result.Do
.bind("user", () => getUser(id))
.bind("role", ({ user }) => getRole(user))
.bind("perms", ({ role }) => getPermissions(role))
.map(({ user, role, perms }) => /* ... */)
The callback receives the accumulated object and returns a Result (or ResultAsync). Short-circuits on Err.
let: add a pure value
Result.Do
.bind("user", () => getUser(id))
.let("greeting", ({ user }) => `Hello, ${user.name}`)
.map(({ user, greeting }) => /* ... */)
For computed values that don't need Result wrapping.
ResultAsync example
Given:
declare function getUser(id: string): ResultAsync<User, GetUserError>
declare function getRole(user: User): ResultAsync<Role, GetRoleError>
ResultAsync.Do
.bind("user", () => getUser(id))
.bind("role", ({ user }) => getRole(user))
.let("label", ({ user, role }) => `${user.name} (${role.name})`)
.map(({ user, role, label }) => ({ user, role, label }))
Design notes
- Added as instance methods on
Ok / Err / ResultAsync, same as andTee / andThrough / orTee
- Accumulator type grows via
{ [K in N | keyof A]: ... } on each bind / let
- Error types accumulate as a union (
E1 | E2 | ...), same as andThen
- Duplicate keys are rejected at the type level with
Exclude<N, keyof A>
Related
Motivation
Chaining multiple dependent operations with
andThenforces nested closures to keep earlier values in scope:safeTrysolves this with generators, but there's demand for a method-chain approach too.fp-ts, Effect, and byethrow all have
Do+bindfor this. Same idea as Haskell's do notation, Scala's for comprehension, F#'s Computation Expressions, and Kotlin/Arrow'seither { }+.bind().Proposed API
Given the following functions:
Entry point
bind: add a value from a Result-returning functionThe callback receives the accumulated object and returns a
Result(orResultAsync). Short-circuits onErr.let: add a pure valueFor computed values that don't need
Resultwrapping.ResultAsync example
Given:
Design notes
Ok/Err/ResultAsync, same asandTee/andThrough/orTee{ [K in N | keyof A]: ... }on eachbind/letE1 | E2 | ...), same asandThenExclude<N, keyof A>Related
*CollectAPI request (closed). Same underlying problem