Skip to content

[PROPOSAL] Do + bind / let for Result and ResultAsync #679

Description

@iwasa-kosui

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions