Skip to content

Proposal: a solution for emulating Rust's ? operator #444

@tsuburin

Description

@tsuburin

In short

The following code emulates the ? operator:

// `returnError` and `block` are utility functions for emulation
function returnError<T, E>(result: Result<T, E>): Generator<Err<never, E>, T> {
  return function*() {
    if (result.isOk()) {
      return result.value
    }
    yield err(result.error) // Calling err(result.error) for changing the type from Err<T, E> to Err<never, E>

    // As long as being consumed by `yield*` in `block`,
    // `next()` of this generator is called just once, so this should never happen
    throw new Error("THIS SHOULD NEVER HAPPEN")
  }()
}

function block<T, E>(
  body: () => Generator<Err<never, E>, Result<T, E>>
) {
  return body().next().value
}


// Example usages

// type of x is Result<"foo", "bar">, and its value is ok("foo")
const x = block<"foo", "bar">(
  function*() {
    const r: Result<"baz", "bar"> = ok("baz")
    const okBaz = yield* returnError(r) // here the type of okBaz is "baz", and the value on execution time is so, too
    return ok("foo") // and finally returns this value
  }
)

// type of y is Result<"foo", "bar">, and its value is err("bar")
const y = block<"foo", "bar">(
  function*() {
    const r: Result<"baz", "bar"> = err("bar")
    const okBaz = yield* returnError(r) // the type of okBaz is "baz", but this block returns err("bar") here
    return ok("foo") // and this line is not evaluated
  }
)

Of course you can use yield*s and returns as many as you want in a block.

Description

An expression yield* gen in a generator function, here gen is a generator, yields what are yielded in gen, and is evaluated to what is returned in gen, without yielding the returned value. (Please see MDN)
As returnError is defined to return a generator that yields the argument if the argument is error and otherwise returns its unwrapped (ok's) value, yield* returnError(r) yields r if r is error, and otherwise is evaluated to r.value without yielding anything.
Finally, as block is defined to call next() on the argument exactly once and return that value, block(function*(){STATEMENTS}) is evaluated to the first occurrence in STATEMENTS of either yield* returnError(some_error) or return some_result.

More integrated version (including async support)

It makes no sense to import returnError or block independently, so I combined them into single object block.
async block is also implemented by overload. It can be used as its type implies.

export const block:
  & (<T, E>(
    body: () => Generator<Err<never, E>, Result<T, E>>
  ) => Result<T, E>)
  & (<T, E>(
    body: () => AsyncGenerator<Err<never, E>, Result<T, E>>
  ) => Promise<Result<T, E>>)
  & {
    returnError: <T, E>(result: Result<T, E>) => Generator<Err<never, E>, T>
  }
  = (() => {
    function fBlock<T, E>(
      body: () => Generator<Err<never, E>, Result<T, E>>
    ): Result<T, E>
    function fBlock<T, E>(
      body: () => AsyncGenerator<Err<never, E>, Result<T, E>>
    ): Promise<Result<T, E>>
    function fBlock<T, E>(
      body:
        | (() => Generator<Err<never, E>, Result<T, E>>)
        | (() => AsyncGenerator<Err<never, E>, Result<T, E>>)
    ) {
      const n = body().next()
      if (n instanceof Promise) {
        return n.then(r => r.value)
      }
      return n.value
    }

    return Object.assign(
      fBlock,
      {
        returnError: <T, E>(result: Result<T, E>): Generator<Err<never, E>, T> => {
          return function*() {
            if (result.isOk()) {
              return result.value
            }
            yield err(result.error)
            throw new Error("THIS SHOULD NEVER HAPPEN")
          }()
        }
      },
    )
  })()

Question

Should I make a PR? I'm willing to make a PR if this is useful for this project.
If so, do anyone have any opinion how to integrate this to the existing APIs? (naming, module structure, or anything else)

Of course, any comments would be appreciated.

Related issues

#183

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