Skip to content

feat(core): add do-notation (Do / bind / let)#29

Merged
btravers merged 5 commits into
mainfrom
feat/do-notation
Jun 27, 2026
Merged

feat(core): add do-notation (Do / bind / let)#29
btravers merged 5 commits into
mainfrom
feat/do-notation

Conversation

@btravers

Copy link
Copy Markdown
Collaborator

Implements fluent do-notation — Do().bind(name, fn).let(name, fn) — in the style of neverthrow #680, adapted to unthrown's three-channel model.

import { Do } from "unthrown";

const view = Do()
  .bind("user", () => findUser(id))               // Result<User, NotFound>
  .bind("org", ({ user }) => findOrg(user.orgId)) // Result<Org, NotFound>
  .let("label", ({ user, org }) => `${user.name} @ ${org.name}`)
  .map(({ user, org, label }) => render(user, org, label));
// Result<View, NotFound>

API

  • Do() — entry point: ok({}), an empty object scope. Capitalised because do is reserved.
  • bind(name, f)f receives the scope so far and returns a Result; on Ok its value is added under name (a readonly accumulating object), on Err/Defect the chain short-circuits. Error types union (E | E2). On AsyncResult, f may return a Result or an AsyncResult (never a raw Promise — Thesis docs: sync to shipped APIs + fix format pre-commit hook #3).
  • let(name, f) — the pure-value counterpart.
  • It's just a Result at every step, so you can interleave map/flatMap/match/etc. To go async, lift with toAsync().

Invariants preserved

A throw in any bind/let callback becomes a Defect; Err/Defect short-circuits/passes through. Both are added to the throw→Defect and Defect-passthrough guards in invariants.spec.ts.

A note on scope

This reverses a stated exclusionCLAUDE.md had do-notation on the "deliberately excluded" list. Per your request I've shipped the fluent form and updated the spec; the generator (gen/safeTry) style remains out of scope. The one cost: bind/let's T & {…} return type forced four casts in interop.ts (the all* family) to widen from as to as unknown as — noted in the diff.

Tests & docs

  • do.spec.ts (sync + async: accumulation, short-circuit, error union, defect passthrough, throw→defect) and three types.test-d.ts assertions (scope accumulation, error union, readonly keys, async parity).
  • New Do Notation guide page + sidebar; added to the method-surface list and the "Choosing a Combinator" cheatsheet; CLAUDE.md surface/invariant/facade updated; minor changeset.
  • Full gate green; 100% core line/function coverage held.

🤖 Generated with Claude Code

Add `Do()` plus `bind`/`let` methods on Result and AsyncResult for
sequencing dependent steps into a named, accumulating object scope —
without nested flatMap closures, and without generators.

- `bind(name, f)`: f returns a Result; its value is bound under `name`
  in a readonly object scope; error types union (E | E2). On AsyncResult,
  f may return a Result or an AsyncResult.
- `let(name, f)`: binds a pure value.
- A throw in either becomes a Defect; Err/Defect short-circuits — same
  invariants as every combinator (guarded in invariants.spec).

`Do` is capitalised because `do` is reserved; lift a sync chain with
`toAsync()` to go async. Runtime + type-level tests, docs (a Do Notation
guide + sidebar/surface/cheatsheet), and CLAUDE.md updated (fluent
do-notation moved off the "excluded" list; generator `gen`/`safeTry`
style stays out). 100% line/function coverage held.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 27, 2026 18:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds fluent do-notation to unthrown’s Result / AsyncResult surface via a new Do() entry point plus bind/let chaining methods, with accompanying tests, documentation, and a changeset.

Changes:

  • Introduces Do() plus bind(name, f) and let(name, f) for sequential, scope-accumulating pipelines on both Result and AsyncResult.
  • Adds runtime + type-level tests to enforce short-circuiting and invariants (throw→Defect, Defect passthrough).
  • Documents the new feature and wires it into the VitePress sidebar and project spec/changeset.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/core/src/types.ts Adds Prettify and extends the public method surface with bind/let typings.
packages/core/src/types.test-d.ts Adds type assertions for scope accumulation, readonly keys, and async parity.
packages/core/src/invariants.spec.ts Extends invariant tests to cover bind/let throw→Defect and Defect passthrough.
packages/core/src/interop.ts Adjusts casts in all* helpers to accommodate widened scope typing interactions.
packages/core/src/index.ts Exports Do from the core package entry point.
packages/core/src/facade.ts Exposes Do on the Result companion facade for discoverability.
packages/core/src/do.ts Implements Do() as the empty-scope seed (ok({})).
packages/core/src/do.spec.ts Adds runtime tests for do-notation behavior (sync + async).
packages/core/src/core.ts Implements bind/let on Result and AsyncResult runtime classes.
docs/guide/do-notation.md Adds a new guide page explaining Do/bind/let (sync + async usage).
docs/guide/core-concepts.md Updates method-surface overview to include do-notation.
docs/guide/choosing-a-combinator.md Adds do-notation to the “Choosing a Combinator” cheatsheet table.
docs/.vitepress/config.ts Adds “Do Notation” to the guide sidebar nav.
CLAUDE.md Updates library spec/invariants and clarifies generator do-notation remains excluded.
.changeset/do-notation.md Publishes a minor changeset describing the new API.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/core/src/types.ts Outdated
Comment thread packages/core/src/types.ts Outdated
Comment thread packages/core/src/core.ts Outdated
Comment thread packages/core/src/core.ts Outdated
Comment thread docs/guide/do-notation.md
btravers and others added 2 commits June 27, 2026 21:38
Address PR review: the scope type used `T & { [K]: U }`, so re-binding an
existing name produced an unsound `T[K] & U` intersection while the
runtime spread overwrites. Introduce `Bound<T, K, U> = Prettify<Omit<T, K>
& { readonly [P in K]: U }>` so the type matches the overwrite. Applied to
both bind/let on Result and AsyncResult, with a type-level test for the
re-bind case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The short-circuit branches reused a non-matching variant (`Err`/`Defect`)
as a differently-typed `Result` via inline `as unknown as` at ~21 sites.
Centralise that single sound assertion in one `passThrough<T, E>(self):
Result<T, E>` helper; every combinator's pass-through (sync + async) now
calls it. `return this` is preserved, so nothing is reconstructed and no
new object is allocated — only the phantom type parameter moves.

Trade-off chosen deliberately: boxed casts inline (scattered),
neverthrow reconstructs (allocates); this keeps boxed's zero-alloc while
collapsing the casts to a single documented home. The only remaining
casts are the bind/let scope merge (a computed key can't be spelled at
the type level) and the builders' construction. Behaviour unchanged;
151 tests + the type-level suite pass, 100% line/function coverage held.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Comment thread packages/core/src/core.ts Outdated
Comment thread packages/core/src/core.ts Outdated
Comment thread packages/core/src/core.ts Outdated
Comment thread packages/core/src/core.ts Outdated
Align the value constructors with the discriminated-union tags and the
capitalized `Do`. Facade (`Result.Ok`/`Err`/`Defect`) and pattern
constructors (`P.Ok`/`Err`/`Defect`) follow. Match handler keys, the
`is*` guards, and the "defect channel" terminology are unchanged. `Err`
(not `Error`) avoids shadowing the global.

BREAKING CHANGE: ok/err/defect renamed to Ok/Err/Defect across unthrown
and @unthrown/pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 52 out of 52 changed files in this pull request and generated 7 comments.

Comments suppressed due to low confidence (1)

packages/effect/README.md:28

  • After renaming constructors to Ok/Err, this snippet still uses ok/err in the .match(...) handlers, but those identifiers are no longer imported/defined, so the example is broken. Use explicit lambdas (or otherwise define handlers) instead of relying on the old constructor names.

Comment thread docs/guide/do-notation.md
Comment thread docs/index.md Outdated
Comment thread packages/core/src/index.ts
Comment thread packages/core/src/types.ts
Comment thread packages/core/src/types.ts
Comment thread packages/core/src/types.ts
Comment thread packages/core/src/types.ts
bind/let live on the general Result surface, so a primitive Ok (e.g.
Ok(5).bind(...)) could reach the scope merge and `{ ...5 }` would
silently collapse to `{}`, dropping the prior scope. Route the merge
through a `scopeOf` guard that throws on a non-object value; the
surrounding try turns it into a Defect — misuse surfaced as the bug it
is, not a silent drop. Covers sync + async bind/let. A `this: object`
constraint was rejected: TS does not hard-enforce a constraint inferred
solely from `this`, and it breaks `AsyncRes implements AsyncResult`.

Also fix two doc snippets to import the constructors they use
(do-notation `Ok`, index `Defect`).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@btravers btravers merged commit 88bbcf4 into main Jun 27, 2026
11 checks passed
@btravers btravers deleted the feat/do-notation branch June 27, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants