Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions src/Traversable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, expect, test } from "bun:test"
import { Option } from "./Option.ts"
import { Result } from "./Result.ts"
import {
sequence,
sequenceResult,
traverse,
traverseResult,
} from "./Traversable.ts"

describe("traverse", () => {
test("with all Some results returns Some of array", () => {
const result = traverse([1, 2, 3], (n) => Option.Some(n * 2))
expect(result.isOk()).toBe(true)
expect(result.orElse([])).toEqual([2, 4, 6])
})

test("with a None result returns None", () => {
const result = traverse([1, 2, 3], (n) =>
n === 2 ? Option.None() : Option.Some(n * 2),
)
expect(result.isOk()).toBe(false)
})
})

describe("sequence", () => {
test("collects all Some values into Some array", () => {
const result = sequence([
Option.Some(1),
Option.Some(2),
Option.Some(3),
])
expect(result.isOk()).toBe(true)
expect(result.orElse([])).toEqual([1, 2, 3])
})

test("with any None returns None", () => {
const result = sequence([
Option.Some(1),
Option.None(),
Option.Some(3),
])
expect(result.isOk()).toBe(false)
})
})

describe("traverseResult", () => {
test("with all Ok returns Ok of array", () => {
const result = traverseResult([1, 2, 3], (n) =>
Result.Ok<number, string>(n * 2),
)
expect(result.isError()).toBe(false)
expect(result.match((v) => v, () => [])).toEqual([2, 4, 6])
})

test("with an Error returns the first Error", () => {
const result = traverseResult([1, 2, 3], (n) =>
n === 2
? Result.Error<number, string>("failed at 2")
: Result.Ok<number, string>(n * 2),
)
expect(result.isError()).toBe(true)
expect(result.match(() => "", (e) => e)).toBe("failed at 2")
})
})

describe("sequenceResult", () => {
test("collects all Ok values into Ok array", () => {
const result = sequenceResult([
Result.Ok<number, string>(1),
Result.Ok<number, string>(2),
Result.Ok<number, string>(3),
])
expect(result.isError()).toBe(false)
expect(result.match((v) => v, () => [])).toEqual([1, 2, 3])
})

test("with any Error returns the first Error", () => {
const result = sequenceResult([
Result.Ok<number, string>(1),
Result.Error<number, string>("oops"),
Result.Ok<number, string>(3),
])
expect(result.isError()).toBe(true)
expect(result.match(() => "", (e) => e)).toBe("oops")
})
})
89 changes: 89 additions & 0 deletions src/Traversable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Option } from "./Option.ts"
import { Result } from "./Result.ts"

/**
* Traversable utilities invert nested structures.
* For example, `Array<Option<T>>` becomes `Option<Array<T>>`,
* enabling "fail-fast" collection processing: if any element
* fails (None or Error), the entire result fails immediately.
*/

/**
* Maps each element through a function returning Option, then flips
* the structure: if all results are Some, returns Some of the array;
* if any is None, returns None.
* @param values - The array of values to traverse
* @param fn - A function mapping each value to an Option
* @returns Option containing the array of results, or None
*/
export function traverse<T, R>(
values: T[],
fn: (value: T) => Option<R>,
): Option<R[]> {
const results: R[] = []
for (const value of values) {
const result = fn(value)
if (!result.isOk()) {
return Option.None()
}
results.push(result.orElse(undefined as never))
}
return Option.Some(results)
}

/**
* Flips an `Option<T>[]` into an `Option<T[]>`.
* If all values are Some, returns Some of the array;
* if any is None, returns None.
* @param values - The array of Options to sequence
* @returns Option containing the array of unwrapped values, or None
*/
export function sequence<T>(values: Option<T>[]): Option<T[]> {
return traverse(values, (v) => v)
}

/**
* Maps each element through a function returning Result, then flips
* the structure: if all results are Ok, returns Ok of the array;
* if any is Error, returns the first Error.
* @param values - The array of values to traverse
* @param fn - A function mapping each value to a Result
* @returns Result containing the array of results, or the first Error
*/
export function traverseResult<T, R, E>(
values: T[],
fn: (value: T) => Result<R, E>,
): Result<R[], E> {
const results: R[] = []
for (const value of values) {
const result = fn(value)
if (result.isError()) {
return Result.Error(
result.match(
() => undefined as never,
(e) => e,
),
)
}
results.push(
result.match(
(v) => v,
() => undefined as never,
),
)
}
return Result.Ok(results)
}

/**
* Flips a `Result<T, E>[]` into a `Result<T[], E>`.
* If all values are Ok, returns Ok of the array;
* if any is Error, returns the first Error.
* @param values - The array of Results to sequence
* @returns Result containing the array of unwrapped values, or the first Error
*/
export function sequenceResult<T, E>(
values: Result<T, E>[],
): Result<T[], E> {
return traverseResult(values, (v) => v)
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from "./Monad.ts"
export * from "./Option.ts"
export * from "./Pipe.ts"
export * from "./Result.ts"
export * from "./Traversable.ts"
Loading