Skip to content

Commit ea72f9d

Browse files
committed
feat: add sequence function to combine Either and Result values with fail-fast behavior
1 parent 5bc22ce commit ea72f9d

File tree

6 files changed

+136
-5
lines changed

6 files changed

+136
-5
lines changed

docs/either/index.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,11 +527,9 @@ Combines an array of `Either` values into a single `Either`. Returns `Right` wit
527527
```ts
528528
import { all, right, left } from 'holo-fn/either';
529529

530-
// All success case
531530
const result1 = all([right(1), right(2), right(3)]);
532531
console.log(result1.unwrapOr([])); // [1, 2, 3]
533532

534-
// Collecting errors
535533
const result2 = all([left('Name required'), left('Email invalid'), right(25)]);
536534
console.log(
537535
result2.match({
@@ -547,3 +545,28 @@ console.log(result3.unwrapOr([])); // []
547545

548546
---
549547

548+
### `sequence`
549+
550+
Combines an array of `Either` values into a single `Either`, stopping at the first error (fail-fast). Returns `Right` with all values if all are `Right`, or `Left` with the first error encountered.
551+
552+
Unlike `all` which collects all errors, `sequence` returns immediately when it finds the first `Left`.
553+
554+
```ts
555+
import { sequence, right, left } from 'holo-fn/either';
556+
557+
const result1 = sequence([right(1), right(2), right(3)]);
558+
console.log(result1.unwrapOr([])); // [1, 2, 3]
559+
560+
const result2 = sequence([
561+
right(1),
562+
left('First error'),
563+
left('Second error')
564+
]);
565+
console.log(result2.match({
566+
left: (e) => e,
567+
right: (v) => v
568+
})); // 'First error' (not an array!)
569+
```
570+
571+
---
572+

docs/result/index.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -424,11 +424,9 @@ Combines an array of `Result` values into a single `Result`. Returns `Ok` with a
424424
```ts
425425
import { all, ok, err } from 'holo-fn/result';
426426

427-
// All success case
428427
const result1 = all([ok(1), ok(2), ok(3)]);
429428
console.log(result1.unwrapOr([])); // [1, 2, 3]
430429

431-
// Collecting errors
432430
const result2 = all([err('Name required'), err('Email invalid'), ok(25)]);
433431
console.log(
434432
result2.match({
@@ -437,10 +435,34 @@ console.log(
437435
})
438436
); // ['Name required', 'Email invalid']
439437

440-
// Empty array
441438
const result3 = all([]);
442439
console.log(result3.unwrapOr([])); // []
443440
```
444441

445442
---
446443

444+
### `sequence`
445+
446+
Combines an array of `Result` values into a single `Result`, stopping at the first error (fail-fast). Returns `Ok` with all values if all are `Ok`, or `Err` with the first error encountered.
447+
448+
Unlike `all` which collects all errors, `sequence` returns immediately when it finds the first `Err`.
449+
450+
```ts
451+
import { sequence, ok, err } from 'holo-fn/result';
452+
453+
const result1 = sequence([ok(1), ok(2), ok(3)]);
454+
console.log(result1.unwrapOr([])); // [1, 2, 3]
455+
456+
const result2 = sequence([
457+
ok(1),
458+
err('First error'),
459+
err('Second error')
460+
]);
461+
console.log(result2.match({
462+
ok: (v) => v,
463+
err: (e) => e
464+
})); // 'First error' (not an array!)
465+
```
466+
467+
---
468+

src/either/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,5 +188,19 @@ export const all = <L, R>(eithers: Either<L, R>[]): Either<L[], R[]> => {
188188
return new Right<L[], R[]>(values);
189189
};
190190

191+
export const sequence = <L, R>(eithers: Either<L, R>[]): Either<L, R[]> => {
192+
const values: R[] = [];
193+
194+
for (const either of eithers) {
195+
if (either.isLeft()) {
196+
return new Left<L, R[]>(either.extract() as L);
197+
}
198+
199+
values.push(either.extract() as R);
200+
}
201+
202+
return new Right<L, R[]>(values);
203+
};
204+
191205
export const left = <L, R = never>(value: L): Either<L, R> => new Left(value);
192206
export const right = <L, R>(value: R): Either<L, R> => new Right(value);

src/result/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,5 +194,19 @@ export const all = <T, E>(results: Result<T, E>[]): Result<T[], E[]> => {
194194
return new Ok<T[], E[]>(values);
195195
};
196196

197+
export const sequence = <T, E>(results: Result<T, E>[]): Result<T[], E> => {
198+
const values: T[] = [];
199+
200+
for (const result of results) {
201+
if (result.isErr()) {
202+
return new Err<T[], E>(result.extract() as E);
203+
}
204+
205+
values.push(result.extract() as T);
206+
}
207+
208+
return new Ok<T[], E>(values);
209+
};
210+
197211
export const ok = <T, E>(value: T): Result<T, E> => new Ok(value);
198212
export const err = <T, E>(error: E): Result<T, E> => new Err(error);

tests/either.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
match,
1414
Right,
1515
right,
16+
sequence,
1617
tryCatch,
1718
unwrapOr,
1819
validate,
@@ -427,4 +428,34 @@ describe('Either - Curried Helpers', () => {
427428
'Age too low',
428429
]);
429430
});
431+
432+
it('sequence should return Right with all values when all are Right', () => {
433+
const eithers = [right<string, number>(1), right<string, number>(2), right<string, number>(3)];
434+
const result = sequence(eithers);
435+
expect(result.isRight()).toBe(true);
436+
expect(result.unwrapOr([])).toEqual([1, 2, 3]);
437+
});
438+
439+
it('sequence should return first Left when any is Left (fail-fast)', () => {
440+
const eithers = [right<string, number>(1), left<string, number>('error1'), left<string, number>('error2')];
441+
const result = sequence(eithers);
442+
expect(result.isLeft()).toBe(true);
443+
expect(result.match({ left: (e) => e, right: (_) => '' })).toBe('error1');
444+
});
445+
446+
it('sequence should return Right with empty array for empty input', () => {
447+
const result = sequence<string, number>([]);
448+
expect(result.isRight()).toBe(true);
449+
expect(result.unwrapOr([])).toEqual([]);
450+
});
451+
452+
it('sequence should stop at first error unlike all', () => {
453+
const eithers = [
454+
left<string, number>('First error'),
455+
left<string, number>('Second error'),
456+
right<string, number>(1),
457+
];
458+
const result = sequence(eithers);
459+
expect(result.match({ left: (e) => e, right: (_) => '' })).toBe('First error');
460+
});
430461
});

tests/result.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
match,
1515
Ok,
1616
ok,
17+
sequence,
1718
unwrapOr,
1819
validate,
1920
} from '../src/result';
@@ -392,4 +393,30 @@ describe('Result - Curried Helpers', () => {
392393
const result = all(results);
393394
expect(result.match({ ok: (_) => [], err: (e) => e })).toEqual(['Name required', 'Email invalid', 'Age too low']);
394395
});
396+
397+
it('sequence should return Ok with all values when all are Ok', () => {
398+
const results = [ok<number, string>(1), ok<number, string>(2), ok<number, string>(3)];
399+
const result = sequence(results);
400+
expect(result.isOk()).toBe(true);
401+
expect(result.unwrapOr([])).toEqual([1, 2, 3]);
402+
});
403+
404+
it('sequence should return first Err when any is Err (fail-fast)', () => {
405+
const results = [ok<number, string>(1), err<number, string>('error1'), err<number, string>('error2')];
406+
const result = sequence(results);
407+
expect(result.isErr()).toBe(true);
408+
expect(result.match({ ok: (_) => '', err: (e) => e })).toBe('error1');
409+
});
410+
411+
it('sequence should return Ok with empty array for empty input', () => {
412+
const result = sequence<number, string>([]);
413+
expect(result.isOk()).toBe(true);
414+
expect(result.unwrapOr([])).toEqual([]);
415+
});
416+
417+
it('sequence should stop at first error unlike all', () => {
418+
const results = [err<number, string>('First error'), err<number, string>('Second error'), ok<number, string>(1)];
419+
const result = sequence(results);
420+
expect(result.match({ ok: (_) => '', err: (e) => e })).toBe('First error');
421+
});
395422
});

0 commit comments

Comments
 (0)