From 0d8d831c824cf37c14c93bae23d302f35bebaea6 Mon Sep 17 00:00:00 2001 From: Daniele Dellafiore <66707+ildella@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:10:55 +0200 Subject: [PATCH 1/2] filter reimplemented on top of series(), wrapping predicate for the undefined short-circuit behavior --- docs/architecture.md | 3 +- docs/guide.md | 12 ++++++++ docs/reference.md | 56 +++++++++++++++++++---------------- src/functional.js | 70 +++++--------------------------------------- tests/filter.test.js | 10 +++---- 5 files changed, 56 insertions(+), 95 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 462adc7..b4afe3f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -33,6 +33,5 @@ The vocabulary we have established for the pipelean project: * Operation: The function passed to an iterator (like series). It can be a simple function or a composed function (pipe). * Transform (Mapping): An operation that changes the shape or value of an item. (A→B). - * Selection (Filtering): An operation that decides whether to keep or drop an item. (A→A or A→∅ - ). In our merged model, this is signaled by returning undefined. + * Selection (Filtering): An operation that decides whether to keep or drop an item. (A→A or A→∅). In our merged model, this is signaled by returning undefined. * Outcome: The structural result returned by iterators: {results, errors, failure}. diff --git a/docs/guide.md b/docs/guide.md index 311fe25..c39ae2d 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -79,6 +79,18 @@ const isValid = pipe( const adults = await filter(isValid, users) ``` +**Undefined Short-Circuit**: When any step returns `undefined`, remaining steps are skipped and `undefined` propagates out. Combined with `series` (which drops items when the operation returns `undefined`), this merges transformation and selection in a single pass: + +```js +import { series, pipe } from 'pipelean' + +const result = await series(numbers, pipe( + x => x % 2 === 0 ? x : undefined, // select: drop odds + x => x * 2, // transform: double +)) +// result.results = [4, 8, 12] from inputs [2, 4, 6] +``` + #### Wrappers Pipelean also provides lightweight wrappers that add behavior to **individual functions**. These act as reusable middleware / lifecycle hooks and compose naturally with `pipe`. diff --git a/docs/reference.md b/docs/reference.md index 0133961..61f7457 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -285,41 +285,34 @@ const { results, errors } = await scan( ### filter -**Purpose**: Stateless selection tool - filters items from an iterable based on a predicate function. +**Purpose**: Stateless selection tool - filters items from an iterable based on a predicate function. Delegates to `series` internally: the predicate is converted to a transform that returns the original item (keep) or `undefined` (drop). -**Type**: `(...args) => filteredItems | filterFunction` +**Type**: `(...args) => Promise | filterFunction` **Parameters**: -- First argument (optional): If a function, specifies `take` -- Remaining arguments: Items to filter -- If first arg is NOT a function: Treated as `iterable` and processed with `take` strategy +- `predicate`: A function `(item, index) => truthy | falsy`, or a plain object pattern (converted via `where()`) +- `items`: The iterable to filter (immediate mode) +- `opts` (optional): Options passed through to `series` -**Options**: -- `strategy`: Error strategy object (`failFast`, `collect`, `failLate`, `skip`, or aliases) -- `onError`: Optional callback called for each error -- `onFailure`: Optional callback called when `failure` is truthy (failFast: `{item, error}`, failLate: `true`) -- `take`: Optional number of items to collect +**Options**: Same as `series` — `strategy`, `onError`, `onFailure`, `take`, `onProgress`. + +**Return Type**: `{ results, errors, failure }` — same shape as `series`: +- `results`: Original items where the predicate returned truthy +- `failure`: `false` on success (no errors), `{item, error}` for `failFast`, `true` for `failLate` -**Return Type**: Returns `{ results, errors, failure }` object (defaults to `collect`): -- With `collect` (default): `{ results, errors: [...], failure: null }` -- With `failFast`: `{ results, errors: [], failure: { item, error } }` -- With `failLate`: `{ results, errors: [...], failure: true }` -- With `skip`: `{ results, errors: [], failure: null }` +**Key Characteristics**: +- The predicate's return value is never placed into `results` — only truthiness is checked, and the original `item` is what gets kept or dropped. +- Pattern objects are supported: `filter({active: true}, users)` works via `where()`. **Usage Example**: ```javascript -import { filter, failFast } from './functional.js' +import { filter } from 'pipelean' -// Filter valid emails from a list (default is collect) -const validEmails = await filter( - async (email) => { - return email.includes('@') - }, - emails, - { - strategy: failFast // Override default to stop on first invalid email - } +const adults = await filter( + user => user.age >= 18, + users, ) +// result.results = [user1, user3, ...] — original items, not predicate output ``` --- @@ -343,6 +336,7 @@ const validEmails = await filter( - Output of one function becomes input to the next - Supports both synchronous and asynchronous functions - Natural data flow from input through transformations +- **Undefined Short-Circuit**: If any step returns `undefined`, remaining steps are skipped and `undefined` is returned. This enables selection (filtering) within a composed pipe — see [series](#series) drop behavior. **Usage Example**: ```javascript @@ -367,6 +361,18 @@ const result = await pipe( **Best Practice**: Use `pipe()` when you need to chain operations that form a coherent data processing pipeline. +**Selection in pipe** (via undefined short-circuit): +```javascript +import { pipe, series } from 'pipelean' + +// Merge filter and transform in a single operation +const result = await series(items, pipe( + x => x.active ? x : undefined, // drop inactive items + x => x.name, // extract name +)) +// Items where active is false are skipped entirely +``` + --- ## Misc diff --git a/src/functional.js b/src/functional.js index 8007e39..9d98793 100644 --- a/src/functional.js +++ b/src/functional.js @@ -137,73 +137,17 @@ export const filter = (...args) => { !Array.isArray(x) const toPredicate = x => isPattern(x) ? where(x) : x const immediate = typeof args[0] !== 'function' && !isPattern(args[0]) - const [ - items, - rawPredicate, - opts, - ] = immediate ? args : [null, args[0], args[1]] + const [items, rawPredicate, opts] = immediate + ? args : [null, args[0], args[1]] const predicate = toPredicate(rawPredicate) - // eslint-disable-next-line complexity, max-statements - const run = async inputItems => { - const { - strategy = collect, - onError: onErrorParam, - take, - onFailure, - } = opts || {} - - const results = [] - const errors = [] - - let index = 0 - let failure = null - - for await (const item of inputItems) { - // eslint-disable-next-line no-undefined - if (take !== undefined && results.length >= take) { - break - } - - try { - const keep = await predicate(item, index) - if (keep) { - results.push(item) - } - } catch (error) { - const strategyName = strategy.name ?? strategy - - if (onErrorParam) { - await onErrorParam(error) - } - - if (strategyName === 'failFast') { - if (onFailure) { - onFailure({item, error}) - } - return {results, errors, failure: {item, error}} - } - - if (strategyName === 'skip') { - index++ - continue - } - - errors.push({item, error}) - } - - index++ - } - - failure = strategy.name === 'failLate' && errors.length > 0 ? true : null - - if (failure && onFailure) { - onFailure(true) - } - - return {results, errors, failure} + const transform = async (item, index) => { + const keep = await predicate(item, index) + // eslint-disable-next-line no-undefined + return keep ? item : undefined } + const run = async inputItems => series(inputItems, transform, opts) return immediate ? run(items) : run } diff --git a/tests/filter.test.js b/tests/filter.test.js index c547ba1..492b8df 100644 --- a/tests/filter.test.js +++ b/tests/filter.test.js @@ -3,12 +3,12 @@ import {filter} from '$src/functional' test('predicate truthy keeps item in results', async () => { const result = await filter([1, 2, 3, 4], x => x > 2) - expect(result).toEqual({results: [3, 4], errors: [], failure: null}) + expect(result).toEqual({results: [3, 4], errors: [], failure: false}) }) test('predicate falsy excludes item without error', async () => { const result = await filter([1, 2, 3], () => false) - expect(result).toEqual({results: [], errors: [], failure: null}) + expect(result).toEqual({results: [], errors: [], failure: false}) }) test('predicate throws with failFast stops and populates failure', async () => { @@ -31,7 +31,7 @@ test('predicate throws with default collect collects errors', async () => { return true }) expect(result.results).toEqual([1, 3]) - expect(result.failure).toBe(null) + expect(result.failure).toBe(false) expect(result.errors).toEqual([ {item: 2, error: bang}, {item: 4, error: bang}, @@ -51,10 +51,10 @@ test('curried form returns a function', () => { test('curried form executes when called with items', async () => { const evens = filter(x => x % 2 === 0) const result = await evens([1, 2, 3, 4]) - expect(result).toEqual({results: [2, 4], errors: [], failure: null}) + expect(result).toEqual({results: [2, 4], errors: [], failure: false}) }) test('empty array returns empty result shape', async () => { const result = await filter([], () => true) - expect(result).toEqual({results: [], errors: [], failure: null}) + expect(result).toEqual({results: [], errors: [], failure: false}) }) From 903fe283dd1d97de2c6e5e51e51bf673983892d9 Mon Sep 17 00:00:00 2001 From: Daniele Dellafiore <66707+ildella@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:11:10 +0200 Subject: [PATCH 2/2] fix lint --- src/functional.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/functional.js b/src/functional.js index 9d98793..ac82935 100644 --- a/src/functional.js +++ b/src/functional.js @@ -138,7 +138,8 @@ export const filter = (...args) => { const toPredicate = x => isPattern(x) ? where(x) : x const immediate = typeof args[0] !== 'function' && !isPattern(args[0]) const [items, rawPredicate, opts] = immediate - ? args : [null, args[0], args[1]] + ? args + : [null, args[0], args[1]] const predicate = toPredicate(rawPredicate) const transform = async (item, index) => { @@ -147,7 +148,7 @@ export const filter = (...args) => { return keep ? item : undefined } - const run = async inputItems => series(inputItems, transform, opts) + const run = inputItems => series(inputItems, transform, opts) return immediate ? run(items) : run }