fn: variadic compose, and curry/CurryableMapper as an extension point - #29
Merged
Conversation
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>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two independent, self-contained changes to
/fn, one commit each.1. Variadic
composecomposewas fixed at 2-ary whilepipechains up to ten steps, and the README already positionscomposeaspipe'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 problempipe'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 tocompose(compose(f1, f2), f3).composenow takes up to ten Mappers, right to left, via an overload set mirroringpipe'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.tsthe way ADR-0004 verifiedpipe: a mismatch-rejection test, an eleventh-Mapper rejection, a ten-Mapper inference chain, and a mixedmaybe/resultchain. The mixed chain surfaced a genuine, worth-documenting difference frompipe:result/mapandresult/andThen's deferred forms are themselves still generic (by design, so they accept a narrowerResultthan configured for), andcomposehas no concrete value in the call for TypeScript to anchor inference against the waypipedoes — so each step needs an explicitMapper<T, U>annotation. This is now documented directly oncompose. Declaration emit was checked directly (not assumed): all nine new overloads reachdist/fn/index.d.ts, and the untyped implementation signature stays uncallable.2.
curryandCurryableMapperreframed as the consumer extension pointBoth 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", andcurry'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/resultifyandcall/resultifyalready use internally (two overloads, an implementation signature with a rest-tuple trailing parameter, a body that spreads intocurry), with a workedscaleByexample. That example is pinned as a runtime test inindex.test.ts, not just prose. No behaviour or signature change; the README's/fnexport 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
/fnwas touched —identity,constant,Fn,Mapper, andpipeare unchanged.maybe/resultcurrying (tracked separately in ADR-0003/README's "Status" section).Test plan
pnpm check(format, lint, typecheck, test:coverage) passes on the worktree branchpnpm buildsucceeds;dist/fn/index.d.tsinspected directly for the newcomposeoverloadscomposechain,scaleBycurryable-combinator examplecomposemismatch rejection, 11th-Mapper rejection, 10-Mapper inference, mixedmaybe/resultchain🤖 Generated with Claude Code