Skip to content

fn: variadic compose, and curry/CurryableMapper as an extension point - #29

Merged
lhellemons merged 2 commits into
mainfrom
design/fn-parity
Aug 4, 2026
Merged

fn: variadic compose, and curry/CurryableMapper as an extension point#29
lhellemons merged 2 commits into
mainfrom
design/fn-parity

Conversation

@lhellemons

Copy link
Copy Markdown
Owner

Summary

Two independent, self-contained changes to /fn, one commit each.

1. Variadic compose

compose was fixed at 2-ary while pipe chains up to ten steps, and the README already positions compose as pipe's deferred counterpart ("for building a reusable function with no value in hand yet"). A two-step ceiling on that counterpart recreated the exact nesting problem pipe's value-first, N-ary redesign was built to kill (issue #7, ADR-0004) — the moment a point-free chain needed a third step, it was back to compose(compose(f1, f2), f3).

compose now takes up to ten Mappers, right to left, via an overload set mirroring pipe's: each step's parameter type is pinned to its neighbour's return type, so a mismatch is a compile error at that step and an eleventh Mapper has no overload to match. Non-breaking — every existing 2-ary call typechecks and behaves exactly as before.

Pinned in index.test-d.ts the way ADR-0004 verified pipe: a mismatch-rejection test, an eleventh-Mapper rejection, a ten-Mapper inference chain, and a mixed maybe/result chain. The mixed chain surfaced a genuine, worth-documenting difference from pipe: result/map and result/andThen's deferred forms are themselves still generic (by design, so they accept a narrower Result than configured for), and compose has no concrete value in the call for TypeScript to anchor inference against the way pipe does — so each step needs an explicit Mapper<T, U> annotation. This is now documented directly on compose. Declaration emit was checked directly (not assumed): all nine new overloads reach dist/fn/index.d.ts, and the untyped implementation signature stays uncallable.

2. curry and CurryableMapper reframed as the consumer extension point

Both were already public, but their docblocks read as an implementation contract rather than a usable API: CurryableMapper's said its union "is not meant to be narrowed at a call site", and curry's said callers must forward arity via a [] | [T] rest tuple or the mechanism silently breaks — accurate, but written for a reader who already knew what these were for.

Per the design decision made in this thread (asked via AskUserQuestion, see below): keep both public, and rewrite the docblocks to present them explicitly as the extension point for a consumer writing their own curryable combinator — mirroring the exact three-part shape promise/resultify and call/resultify already use internally (two overloads, an implementation signature with a rest-tuple trailing parameter, a body that spreads into curry), with a worked scaleBy example. That example is pinned as a runtime test in index.test.ts, not just prose. No behaviour or signature change; the README's /fn export table is unaffected.

AskUserQuestion answer received

"curry and CurryableMapper are documented as an implementation contract, not really consumable at a call site... Should they stay public?" → "Keep public, reframe as extension point (recommended)".

Left out / not touched

  • Nothing else in /fn was touched — identity, constant, Fn, Mapper, and pipe are unchanged.
  • No change to maybe/result currying (tracked separately in ADR-0003/README's "Status" section).

Test plan

  • pnpm check (format, lint, typecheck, test:coverage) passes on the worktree branch
  • pnpm build succeeds; dist/fn/index.d.ts inspected directly for the new compose overloads
  • New runtime tests: 10-Mapper compose chain, scaleBy curryable-combinator example
  • New type-level tests: compose mismatch rejection, 11th-Mapper rejection, 10-Mapper inference, mixed maybe/result chain

🤖 Generated with Claude Code

lhellemons and others added 2 commits August 4, 2026 09:00
compose was fixed at 2-ary while pipe chains up to ten steps, so the
README's own framing of compose as pipe's deferred counterpart ("for
building a reusable function with no value in hand yet") recreated the
nesting problem pipe was redesigned to kill (#7, ADR-0004) the moment a
point-free chain needed a third step.

Overloads mirror pipe's per-arity set: each step's parameter type is
pinned to its neighbour's return type, so a mismatch is a compile error
at that step, and an eleventh Mapper has no overload to match. Pinned in
index.test-d.ts with a mismatch-rejection test, an eleventh-Mapper
rejection, a ten-Mapper inference chain, and a mixed maybe/result chain
(the compose analogue of pipe's ADR-0004 verification) — the mixed chain
needs each step annotated with an explicit Mapper<T, U>, documented on
compose itself, because result/map and result/andThen's deferred forms
are themselves still generic, and compose has no concrete value in the
call for TypeScript to anchor inference against the way pipe does.
Declaration emit checked directly: all nine new overloads reach
dist/fn/index.d.ts, and the untyped implementation signature stays
uncallable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both docblocks described curry's rest-tuple arity rule and
CurryableMapper's non-narrowing union accurately, but only from the
perspective of someone already building a curryable combinator inside
this library — CurryableMapper's own docblock said its union "is not
meant to be narrowed at a call site" and curry's said callers must
forward arity via a [] | [T] rest tuple or the mechanism silently
breaks, with no worked example of doing that correctly.

Per the design decision (kept public, recommended option): both stay
exported. Their docblocks now present the exact three-part shape
promise/resultify and call/resultify already use — two overloads, an
implementation signature with a rest-tuple trailing parameter, a body
that spreads into curry — with a worked scaleBy example, pinned as a
runtime test in index.test.ts. No behaviour or signature change; the
README's /fn export table is unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lhellemons
lhellemons merged commit 5a11415 into main Aug 4, 2026
1 check passed
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.

1 participant