diff --git a/CHANGELOG.md b/CHANGELOG.md index 15a33e75c..aab05191e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ history. ## Unreleased +- `types/sorted_list`: the two merge tail policies are now named `keepTail` and + `dropTail` instead of sharing one shadowed name; internal only + [#1546](https://github.com/functionalscript/functionalscript/pull/1546) - `types/bit_vec`: `tryListToVec`/`tryU8ListToVec` reuse the shared balanced fold, at the same cost as the accumulator they replace [#1548](https://github.com/functionalscript/functionalscript/pull/1548) diff --git a/fjs/types/function/todo/uncurry-accumulator-types.md b/fjs/types/function/todo/uncurry-accumulator-types.md index 468615f6b..912aa0408 100644 --- a/fjs/types/function/todo/uncurry-accumulator-types.md +++ b/fjs/types/function/todo/uncurry-accumulator-types.md @@ -35,7 +35,7 @@ This removes the partial-application footgun and drops one closure allocation pe ### Considerations -- **Broad mechanical refactor.** ~20+ operator definitions across `bigint`, `prime_field`, `bit_vec`, `string`, `monoid`, `number`, `range_map`, plus `operator` itself and the `foldToScan`/`reduceToScan`/`fold`/`reduce` plumbing in `list`. `genericMerge`/`cmpReduce`/`mergeTail` in `sorted_list` and the `range_map` merge consumers change accordingly. +- **Broad mechanical refactor.** ~20+ operator definitions across `bigint`, `prime_field`, `bit_vec`, `string`, `monoid`, `number`, `range_map`, plus `operator` itself and the `foldToScan`/`reduceToScan`/`fold`/`reduce` plumbing in `list`. `genericMerge`/`cmpReduce`/`keepTail` in `sorted_list` and the `range_map` merge consumers change accordingly. - **`Fold` can no longer be `Binary`.** This draws a clean line between combinators where currying is genuinely useful (`Binary`/`Equal`/`Unary`) and accumulators where currying is dangerous (`Fold`/`Reduce`/`StateScan`/`ReduceOp`/`TailReduce`). - Could be split: `Fold`/`Reduce` first, `sorted_list`'s `ReduceOp`/`TailReduce` as a follow-up. diff --git a/fjs/types/sorted_list/module.f.mjs b/fjs/types/sorted_list/module.f.mjs index f8fa2fa70..3838149ea 100644 --- a/fjs/types/sorted_list/module.f.mjs +++ b/fjs/types/sorted_list/module.f.mjs @@ -5,7 +5,7 @@ * * @import { Cmp } from '../function/compare/types.ts' * @import { List } from '../list/types.ts' - * @import { ReduceOp, SortedList, TailReduce, _MergeReduce } from './types.ts' + * @import { ReduceOp, SortedList, _MergeReduce } from './types.ts' */ import { bsearch } from '../function/compare/module.f.mjs' @@ -54,11 +54,7 @@ export const merge = * @param {Cmp} cmp * @returns {(a: SortedList) => (b: SortedList) => SortedList} */ - cmp => { - /** @type {TailReduce} */ - const tailReduce = mergeTail - return genericMerge({ reduceOp: cmpReduce(cmp), tailReduce })(null) - } + cmp => genericMerge({ reduceOp: cmpReduce(cmp), tailReduce: keepTail })(null) const cmpReduce = /** @@ -71,10 +67,22 @@ const cmpReduce = return [sign === 1 ? b : a, sign, null] } -/** @type {() => (tail: List) => List} */ -const mergeTail = () => identity +/** + * The two tail policies `genericMerge` takes, named for what they do with the + * list that has not been exhausted yet: `merge` keeps it, because everything + * still there belongs in the union, and `intersect` drops it, because with one + * side exhausted nothing remaining can be matched. + * + * Both are left to inference deliberately. An explicit + * `() => (tail: List) => List` compiles here but introduces its own + * type parameter, which `genericMerge` then collects as an inference candidate + * alongside `reduceOp`'s; the best common supertype of the two is `unknown`, so + * `T` widens and both call sites fail to match their declared return type. With + * no annotation there is no second candidate and `reduceOp` alone fixes `T`. + */ +const keepTail = () => identity -const tailReduce = () => () => null +const dropTail = () => () => null const intersectReduce = /** @@ -93,7 +101,7 @@ export const intersect = * @param {Cmp} cmp * @returns {(a: SortedList) => (b: SortedList) => SortedList} */ - cmp => genericMerge({ reduceOp: intersectReduce(cmp), tailReduce })(null) + cmp => genericMerge({ reduceOp: intersectReduce(cmp), tailReduce: dropTail })(null) export const find = /** diff --git a/fjs/types/sorted_list/todo/tail-reduce-shadowing.md b/fjs/types/sorted_list/todo/tail-reduce-shadowing.md deleted file mode 100644 index 71396c4c5..000000000 --- a/fjs/types/sorted_list/todo/tail-reduce-shadowing.md +++ /dev/null @@ -1,44 +0,0 @@ -## Two opposite `tailReduce`s share one name - -**Priority:** P3 -**Status:** open - -### Problem - -`module.f.mjs` binds the name `tailReduce` twice with contradictory meanings, -one shadowing the other: - -```js -export const merge = cmp => { - /** @type {TailReduce} */ - const tailReduce = mergeTail // :58 — keeps the remaining tail - return genericMerge({ reduceOp: cmpReduce(cmp), tailReduce })(null) -} -const mergeTail = () => identity // :74 -const tailReduce = () => () => null // :76 — discards the tail -export const intersect = cmp => - genericMerge({ reduceOp: intersectReduce(cmp), tailReduce })(null) // :95 -``` - -A reader at `:59` and a reader at `:95` see the same identifier meaning -opposite tail policies. The local binding exists only to carry a JSDoc -annotation; it captures nothing, so §6.3 says hoist it. - -### Proposal - -Rename the two to say what they do (`keepTail` / `dropTail`), annotate -`keepTail` at module scope, and delete the shadowing local. `merge` and -`intersect` then read as the same shape differing only in `reduceOp` and -tail policy. - -### Tasks - -- [ ] Rename `mergeTail` → `keepTail` (with the module-scope annotation) and - the module-level `tailReduce` → `dropTail` -- [ ] Remove the shadowing local in `merge` - -### Related - -- [66b-sorted-list-cmp-reduce-factory](../../todo/66b-sorted-list-cmp-reduce-factory.md) - — covers `cmpReduce` vs `intersectReduce`; this issue covers the tail - policies diff --git a/todo/migrate-typescript-to-mjs.md b/todo/migrate-typescript-to-mjs.md index 1dbe54bea..023ddcd77 100644 --- a/todo/migrate-typescript-to-mjs.md +++ b/todo/migrate-typescript-to-mjs.md @@ -558,7 +558,7 @@ data. A related mechanical finding from the same review round: composing multiple independently-generic helper functions inside another generic function's body -(e.g. `genericMerge` calling `cmpReduce` calling into `mergeTail`, all +(e.g. `genericMerge` calling `cmpReduce` calling into `keepTail`, all separately ``-generic) loses type inference when each is annotated with a single `@type {(...) => ...}` on the whole arrow chain — TypeScript cannot always unify the type parameters across the nested generic-value calls,