- Introduction & Core Concepts
- Basic Usage Patterns
- Real-World Examples
- Advanced Features Deep Dive
- Practical Patterns & Best Practices
- Integration Examples
Arrays in JavaScript are mutable, index-based, and optimized for random access. However, many functional programming patterns work better with immutable, recursive data structures. Traditional array operations often lead to performance issues when dealing with prepending, recursive algorithms, or persistent data structures:
// Traditional approach - inefficient recursive processing with arrays
function processNestedStructure(items: Item[], accumulator: Result[] = []): Result[] {
if (items.length === 0) return accumulator;
const [head, ...tail] = items; // O(n) operation - creates new array
// Process head element
const processed = transformItem(head);
// Prepending is O(n) with arrays
const newAccumulator = [processed, ...accumulator]; // Another O(n) operation
// Recursive call with array destructuring is inefficient
return processNestedStructure(tail, newAccumulator);
}
// Stack-unsafe recursion with arrays
function findPath(graph: Node[], target: string, visited: string[] = []): string[] | null {
for (const node of graph) {
if (visited.includes(node.id)) continue; // O(n) lookup
if (node.id === target) {
return [...visited, node.id]; // O(n) array copy
}
const newVisited = [...visited, node.id]; // Another O(n) copy
const path = findPath(node.children, target, newVisited);
if (path) return path;
}
return null;
}This approach leads to:
- Performance degradation - Array destructuring and spreading create O(n) copies
- Stack overflow risks - No tail-call optimization for recursive algorithms
- Memory inefficiency - Copying arrays for immutability is expensive
- Poor prepend performance - Arrays are optimized for append, not prepend operations
Effect's List module provides an immutable, persistent linked list that excels at functional programming patterns, recursive algorithms, and prepend operations:
import { List, Option } from "effect"
// Efficient recursive processing with List
const processNestedStructure = <A, B>(
items: List.List<A>,
transform: (a: A) => B
): List.List<B> => {
return List.reverse(
List.reduce(items, List.empty<B>(), (acc, item) =>
List.prepend(acc, transform(item)) // O(1) prepend operation
)
)
}
// Stack-safe recursion with List
const findPath = (
graph: List.List<Node>,
target: string
): Option.Option<List.List<string>> => {
const go = (
nodes: List.List<Node>,
visited: List.List<string>
): Option.Option<List.List<string>> => {
return List.findFirst(nodes, (node) => {
if (List.some(visited, (id) => id === node.id)) {
return Option.none()
}
if (node.id === target) {
return Option.some(List.prepend(visited, node.id))
}
return go(node.children, List.prepend(visited, node.id))
})
}
return go(graph, List.empty())
}Persistent Data Structure: List is immutable - operations return new lists without modifying the original, enabling safe sharing and time-travel debugging.
Head/Tail Decomposition: Lists are either empty or consist of a head element and a tail list, perfect for recursive algorithms and pattern matching.
Structural Sharing: Lists share common suffixes, making operations like prepend extremely memory-efficient.
Lazy Evaluation: Many List operations are lazy, allowing efficient composition without intermediate allocations.
import { List, ReadonlyArray } from "effect"
// Create empty list
const empty = List.empty<number>()
// Create list from elements
const numbers = List.make(1, 2, 3, 4, 5)
// Create list from array
const fromArray = List.fromIterable([1, 2, 3, 4, 5])
// Convert list to array
const toArray = List.toArray(numbers) // [1, 2, 3, 4, 5]
// Create list with cons (prepend)
const consed = List.cons(0, numbers) // List(0, 1, 2, 3, 4, 5)
// Create list with range
const range = List.range(1, 10) // List(1, 2, 3, ..., 10)import { List, Option, pipe } from "effect"
const users = List.make(
{ id: 1, name: "Alice", age: 30 },
{ id: 2, name: "Bob", age: 25 },
{ id: 3, name: "Charlie", age: 35 }
)
// Head and tail access
const first = List.head(users) // Option.some({ id: 1, name: "Alice", age: 30 })
const rest = List.tail(users) // Option.some(List({ id: 2, ... }, { id: 3, ... }))
// Safe indexing
const secondUser = List.get(users, 1) // Option.some({ id: 2, name: "Bob", age: 25 })
const outOfBounds = List.get(users, 10) // Option.none()
// Length and emptiness
const count = List.length(users) // 3
const isEmpty = List.isEmpty(users) // false
const isNonEmpty = List.isNonEmpty(users) // trueimport { List, pipe } from "effect"
interface Product {
id: number
name: string
price: number
category: string
}
const products = List.make(
{ id: 1, name: "Laptop", price: 999, category: "Electronics" },
{ id: 2, name: "Coffee", price: 5, category: "Food" },
{ id: 3, name: "Book", price: 20, category: "Education" },
{ id: 4, name: "Phone", price: 699, category: "Electronics" }
)
// Map transformation
const productNames = List.map(products, (p) => p.name) // List("Laptop", "Coffee", "Book", "Phone")
// Filter operation
const electronics = List.filter(products, (p) => p.category === "Electronics") // List(laptop, phone)
// FlatMap for nested transformations
const expandedProducts = List.flatMap(products, (p) =>
List.make(
{ ...p, variant: "Standard" },
{ ...p, variant: "Premium", price: p.price * 1.5 }
)
)A common pattern in applications is processing tasks in FIFO order while maintaining history:
import { List, Option, Effect, pipe } from "effect"
interface Task {
id: string
priority: "low" | "medium" | "high"
action: () => Effect.Effect<void>
createdAt: Date
}
class TaskQueue {
private pending = List.empty<Task>()
private completed = List.empty<Task>()
private failed = List.empty<{ task: Task; error: unknown }>()
enqueue(task: Task): void {
// O(1) prepend for FIFO via reverse
this.pending = List.prepend(this.pending, task)
}
enqueueBatch(tasks: ReadonlyArray<Task>): void {
this.pending = List.prependAll(this.pending, List.fromIterable(tasks))
}
async processNext(): Promise<Option.Option<Task>> {
// Get tasks in FIFO order
const fifoTasks = List.reverse(this.pending)
const next = List.head(fifoTasks)
if (Option.isNone(next)) {
return Option.none()
}
const task = next.value
const remaining = List.tail(fifoTasks)
this.pending = Option.getOrElse(remaining, () => List.empty<Task>())
try {
await Effect.runPromise(task.action())
this.completed = List.prepend(this.completed, task)
return Option.some(task)
} catch (error) {
this.failed = List.prepend(this.failed, { task, error })
throw error
}
}
async processAll(): Promise<void> {
while (List.isNonEmpty(this.pending)) {
await this.processNext()
}
}
getHistory(): ReadonlyArray<Task> {
return List.toArray(List.reverse(this.completed))
}
getFailures(): ReadonlyArray<{ task: Task; error: unknown }> {
return List.toArray(List.reverse(this.failed))
}
getPendingByPriority(): Record<Task["priority"], ReadonlyArray<Task>> {
return pipe(
List.groupBy((task) => task.priority)(List.reverse(this.pending)),
(groups) => ({
high: List.toArray(groups.get("high") ?? List.empty()),
medium: List.toArray(groups.get("medium") ?? List.empty()),
low: List.toArray(groups.get("low") ?? List.empty())
})
)
}
}
// Usage
const queue = new TaskQueue()
queue.enqueueBatch([
{
id: "task-1",
priority: "high",
action: () => Effect.log("Processing high priority task"),
createdAt: new Date()
},
{
id: "task-2",
priority: "low",
action: () => Effect.log("Processing low priority task"),
createdAt: new Date()
}
])
await queue.processAll()
console.log("Completed tasks:", queue.getHistory())Lists excel at implementing undo/redo functionality due to their efficient prepend operations:
import { List, Option, pipe } from "effect"
interface Command<T> {
execute: (state: T) => T
undo: (state: T) => T
description: string
}
class UndoRedoStack<T> {
private undoStack = List.empty<Command<T>>()
private redoStack = List.empty<Command<T>>()
constructor(private state: T) {}
execute(command: Command<T>): T {
// Execute command
this.state = command.execute(this.state)
// Add to undo stack (O(1))
this.undoStack = List.prepend(this.undoStack, command)
// Clear redo stack
this.redoStack = List.empty()
return this.state
}
undo(): Option.Option<T> {
return pipe(
List.head(this.undoStack),
Option.map((command) => {
// Remove from undo stack
this.undoStack = Option.getOrElse(
List.tail(this.undoStack),
() => List.empty<Command<T>>()
)
// Apply undo
this.state = command.undo(this.state)
// Add to redo stack
this.redoStack = List.prepend(this.redoStack, command)
return this.state
})
)
}
redo(): Option.Option<T> {
return pipe(
List.head(this.redoStack),
Option.map((command) => {
// Remove from redo stack
this.redoStack = Option.getOrElse(
List.tail(this.redoStack),
() => List.empty<Command<T>>()
)
// Apply redo
this.state = command.execute(this.state)
// Add back to undo stack
this.undoStack = List.prepend(this.undoStack, command)
return this.state
})
)
}
getState(): T {
return this.state
}
getHistory(): ReadonlyArray<string> {
return pipe(
List.reverse(this.undoStack),
List.map((cmd) => cmd.description),
List.toArray
)
}
canUndo(): boolean {
return List.isNonEmpty(this.undoStack)
}
canRedo(): boolean {
return List.isNonEmpty(this.redoStack)
}
}
// Example: Text editor with undo/redo
interface EditorState {
content: string
cursor: number
}
const insertText = (text: string, position: number): Command<EditorState> => ({
execute: (state) => ({
content: state.content.slice(0, position) + text + state.content.slice(position),
cursor: position + text.length
}),
undo: (state) => ({
content: state.content.slice(0, position) + state.content.slice(position + text.length),
cursor: position
}),
description: `Insert "${text}" at position ${position}`
})
const deleteText = (start: number, length: number): Command<EditorState> => {
let deletedText = ""
return {
execute: (state) => {
deletedText = state.content.slice(start, start + length)
return {
content: state.content.slice(0, start) + state.content.slice(start + length),
cursor: start
}
},
undo: (state) => ({
content: state.content.slice(0, start) + deletedText + state.content.slice(start),
cursor: start + deletedText.length
}),
description: `Delete ${length} characters at position ${start}`
}
}
// Usage
const editor = new UndoRedoStack<EditorState>({ content: "", cursor: 0 })
editor.execute(insertText("Hello ", 0))
editor.execute(insertText("World", 6))
editor.execute(deleteText(5, 1)) // Delete space
console.log(editor.getState()) // { content: "HelloWorld", cursor: 5 }
Option.match(editor.undo(), {
onNone: () => console.log("Nothing to undo"),
onSome: (state) => console.log("After undo:", state) // { content: "Hello World", cursor: 6 }
})
Option.match(editor.redo(), {
onNone: () => console.log("Nothing to redo"),
onSome: (state) => console.log("After redo:", state) // { content: "HelloWorld", cursor: 5 }
})Lists are ideal for tracking paths through tree structures:
import { List, Option, Either, pipe } from "effect"
interface FileNode {
name: string
type: "file" | "directory"
children: List.List<FileNode>
size: number
}
class FileSystem {
constructor(private root: FileNode) {}
findFile(fileName: string): Option.Option<List.List<string>> {
const search = (
node: FileNode,
path: List.List<string>
): Option.Option<List.List<string>> => {
const currentPath = List.prepend(path, node.name)
if (node.type === "file" && node.name === fileName) {
return Option.some(List.reverse(currentPath))
}
if (node.type === "directory") {
return pipe(
node.children,
List.findFirstMap((child) => search(child, currentPath))
)
}
return Option.none()
}
return search(this.root, List.empty())
}
getAllPaths(): List.List<string> {
const collect = (
node: FileNode,
path: List.List<string>
): List.List<string> => {
const currentPath = List.prepend(path, node.name)
const pathString = currentPath.pipe(
List.reverse,
List.join("/")
)
if (node.type === "file") {
return List.of(pathString)
}
return pipe(
List.of(pathString),
List.appendAll(
pipe(
node.children,
List.flatMap((child) => collect(child, currentPath))
)
)
)
}
return collect(this.root, List.empty())
}
calculateSize(path: string): Either.Either<number, string> {
const pathParts = path.split("/").filter(Boolean)
const navigate = (
node: FileNode,
remaining: List.List<string>
): Either.Either<number, string> => {
if (List.isEmpty(remaining)) {
return Either.right(calculateNodeSize(node))
}
return pipe(
List.head(remaining),
Option.flatMap((nextPart) =>
List.findFirst(node.children, (child) => child.name === nextPart)
),
Option.match({
onNone: () => Either.left(`Path not found: ${path}`),
onSome: (child) => navigate(
child,
Option.getOrElse(List.tail(remaining), () => List.empty())
)
})
)
}
const calculateNodeSize = (node: FileNode): number => {
if (node.type === "file") return node.size
return pipe(
node.children,
List.reduce(0, (acc, child) => acc + calculateNodeSize(child))
)
}
return navigate(this.root, List.fromIterable(pathParts))
}
}
// Example usage
const fileSystem = new FileSystem({
name: "root",
type: "directory",
size: 0,
children: List.make(
{
name: "src",
type: "directory",
size: 0,
children: List.make(
{ name: "index.ts", type: "file", size: 1024, children: List.empty() },
{ name: "utils.ts", type: "file", size: 512, children: List.empty() }
)
},
{
name: "docs",
type: "directory",
size: 0,
children: List.make(
{ name: "README.md", type: "file", size: 2048, children: List.empty() }
)
}
)
})
// Find a file
Option.match(fileSystem.findFile("utils.ts"), {
onNone: () => console.log("File not found"),
onSome: (path) => console.log("Found at:", List.toArray(path)) // ["root", "src", "utils.ts"]
})
// Get all paths
const allPaths = List.toArray(fileSystem.getAllPaths())
console.log("All paths:", allPaths)
// Calculate directory size
Either.match(fileSystem.calculateSize("root/src"), {
onLeft: (error) => console.error(error),
onRight: (size) => console.log("Size of src:", size) // 1536
})Event sourcing patterns benefit from List's immutable append operations and efficient replay:
import { List, Option, Effect, pipe, DateTime, Order } from "effect"
interface Event {
id: string
type: string
timestamp: DateTime.DateTime
payload: unknown
}
interface Snapshot<T> {
version: number
state: T
timestamp: DateTime.DateTime
}
class EventStore<T> {
private events = List.empty<Event>()
private snapshots = List.empty<Snapshot<T>>()
constructor(
private readonly reducer: (state: T, event: Event) => T,
private readonly initialState: T,
private readonly snapshotInterval: number = 100
) {}
append(event: Event): T {
// Append event (O(1) with reverse later)
this.events = List.prepend(this.events, event)
// Check if we need a snapshot
if (List.length(this.events) % this.snapshotInterval === 0) {
this.createSnapshot()
}
return this.getCurrentState()
}
appendBatch(events: ReadonlyArray<Event>): T {
// Efficient batch append
this.events = List.prependAll(
this.events,
List.reverse(List.fromIterable(events))
)
const newEventCount = List.length(this.events)
const lastSnapshotCount = Option.match(List.head(this.snapshots), {
onNone: () => 0,
onSome: (snapshot) => snapshot.version
})
if (newEventCount - lastSnapshotCount >= this.snapshotInterval) {
this.createSnapshot()
}
return this.getCurrentState()
}
private createSnapshot(): void {
const state = this.getCurrentState()
const version = List.length(this.events)
this.snapshots = List.prepend(this.snapshots, {
version,
state,
timestamp: DateTime.now
})
// Keep only last 5 snapshots
this.snapshots = List.take(this.snapshots, 5)
}
getCurrentState(): T {
// Find the most recent snapshot
const snapshot = List.head(this.snapshots)
return Option.match(snapshot, {
onNone: () => {
// No snapshot, replay all events
return pipe(
List.reduce(this.initialState, this.reducer)(List.reverse(this.events))
)
},
onSome: (snap) => {
// Replay events after snapshot
const eventsAfterSnapshot = pipe(
List.take(List.length(this.events) - snap.version)(List.reverse(this.events))
)
return pipe(
List.reduce(snap.state, this.reducer)(eventsAfterSnapshot)
)
}
})
}
getEventHistory(
since?: DateTime.DateTime,
until?: DateTime.DateTime
): List.List<Event> {
return pipe(
List.filter((event) => {
if (since && DateTime.lessThan(event.timestamp, since)) return false
if (until && DateTime.greaterThan(event.timestamp, until)) return false
return true
})(List.reverse(this.events))
)
}
replayFrom(timestamp: DateTime.DateTime): T {
const relevantEvents = pipe(
List.filter((event) => DateTime.greaterThanOrEqualTo(event.timestamp, timestamp))(List.reverse(this.events))
)
return List.reduce(relevantEvents, this.initialState, this.reducer)
}
fork(): EventStore<T> {
const forked = new EventStore(
this.reducer,
this.initialState,
this.snapshotInterval
)
// Share immutable event history
forked.events = this.events
forked.snapshots = this.snapshots
return forked
}
}
// Example: Order management system
interface OrderState {
orders: Map<string, Order>
revenue: number
itemsSold: number
}
interface Order {
id: string
customerId: string
items: List.List<OrderItem>
status: "pending" | "confirmed" | "shipped" | "delivered" | "cancelled"
total: number
}
interface OrderItem {
productId: string
quantity: number
price: number
}
type OrderEvent =
| { type: "OrderCreated"; orderId: string; customerId: string; items: OrderItem[] }
| { type: "OrderConfirmed"; orderId: string }
| { type: "OrderShipped"; orderId: string; trackingNumber: string }
| { type: "OrderDelivered"; orderId: string }
| { type: "OrderCancelled"; orderId: string; reason: string }
const orderReducer = (state: OrderState, event: Event): OrderState => {
const payload = event.payload as OrderEvent
switch (payload.type) {
case "OrderCreated": {
const order: Order = {
id: payload.orderId,
customerId: payload.customerId,
items: List.fromIterable(payload.items),
status: "pending",
total: payload.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
return {
...state,
orders: new Map(state.orders).set(order.id, order)
}
}
case "OrderConfirmed": {
const order = state.orders.get(payload.orderId)
if (!order) return state
const updatedOrder = { ...order, status: "confirmed" as const }
return {
...state,
orders: new Map(state.orders).set(order.id, updatedOrder),
revenue: state.revenue + order.total,
itemsSold: state.itemsSold + List.length(order.items)
}
}
// ... other event handlers
default:
return state
}
}
// Usage
const orderStore = new EventStore<OrderState>(
orderReducer,
{ orders: new Map(), revenue: 0, itemsSold: 0 },
50 // Snapshot every 50 events
)
// Process events
orderStore.append({
id: "evt-1",
type: "OrderCreated",
timestamp: DateTime.now,
payload: {
type: "OrderCreated",
orderId: "order-123",
customerId: "customer-456",
items: [
{ productId: "prod-1", quantity: 2, price: 29.99 },
{ productId: "prod-2", quantity: 1, price: 49.99 }
]
}
})
// Get analytics for a time period
const lastWeekEvents = orderStore.getEventHistory(
DateTime.subtract(DateTime.now, { days: 7 }),
DateTime.now
)
// Fork for what-if analysis
const analysisStore = orderStore.fork()Lists excel at graph traversal algorithms where paths need to be tracked:
import { List, Option, HashSet, HashMap, pipe, Equal } from "effect"
interface Graph<T> {
vertices: HashSet.HashSet<T>
edges: HashMap.HashMap<T, List.List<T>>
}
class GraphAlgorithms<T> {
constructor(
private readonly graph: Graph<T>,
private readonly equals: Equal.Equal<T>
) {}
// Depth-first search with path tracking
dfs(start: T, target: T): Option.Option<List.List<T>> {
const visited = new Set<T>()
const search = (current: T, path: List.List<T>): Option.Option<List.List<T>> => {
if (this.equals(current, target)) {
return Option.some(List.reverse(List.prepend(path, current)))
}
visited.add(current)
const currentPath = List.prepend(path, current)
const neighbors = HashMap.get(this.graph.edges, current)
return Option.flatMap(neighbors, (neighborList) =>
List.findFirstMap(neighborList, (neighbor) => {
if (visited.has(neighbor)) return Option.none()
return search(neighbor, currentPath)
})
)
}
return search(start, List.empty())
}
// Find all paths between two vertices
allPaths(start: T, target: T, maxLength?: number): List.List<List.List<T>> {
const findPaths = (
current: T,
path: List.List<T>,
visited: HashSet.HashSet<T>
): List.List<List.List<T>> => {
if (this.equals(current, target)) {
return List.of(List.reverse(List.prepend(path, current)))
}
if (maxLength && List.length(path) >= maxLength) {
return List.empty()
}
const newVisited = HashSet.add(visited, current)
const currentPath = List.prepend(path, current)
return pipe(
HashMap.get(this.graph.edges, current),
Option.getOrElse(() => List.empty<T>()),
List.filter((neighbor) => !HashSet.has(newVisited, neighbor)),
List.flatMap((neighbor) => findPaths(neighbor, currentPath, newVisited))
)
}
return findPaths(start, List.empty(), HashSet.empty())
}
// Topological sort using DFS
topologicalSort(): Option.Option<List.List<T>> {
const visited = new Set<T>()
const finished = new Set<T>()
let sorted = List.empty<T>()
let hasCycle = false
const visit = (vertex: T): void => {
if (finished.has(vertex)) return
if (visited.has(vertex)) {
hasCycle = true
return
}
visited.add(vertex)
const neighbors = HashMap.get(this.graph.edges, vertex)
Option.match(neighbors, {
onNone: () => {},
onSome: (neighborList) => {
List.forEach(neighborList, visit)
}
})
finished.add(vertex)
sorted = List.prepend(sorted, vertex)
}
HashSet.forEach(this.graph.vertices, visit)
return hasCycle ? Option.none() : Option.some(sorted)
}
// Find strongly connected components
stronglyConnectedComponents(): List.List<List.List<T>> {
const index = new Map<T, number>()
const lowlink = new Map<T, number>()
const onStack = new Set<T>()
let stack = List.empty<T>()
let currentIndex = 0
let components = List.empty<List.List<T>>()
const strongConnect = (v: T): void => {
index.set(v, currentIndex)
lowlink.set(v, currentIndex)
currentIndex++
stack = List.prepend(stack, v)
onStack.add(v)
const neighbors = HashMap.get(this.graph.edges, v)
Option.match(neighbors, {
onNone: () => {},
onSome: (neighborList) => {
List.forEach(neighborList, (w) => {
if (!index.has(w)) {
strongConnect(w)
lowlink.set(v, Math.min(lowlink.get(v)!, lowlink.get(w)!))
} else if (onStack.has(w)) {
lowlink.set(v, Math.min(lowlink.get(v)!, index.get(w)!))
}
})
}
})
if (lowlink.get(v) === index.get(v)) {
let component = List.empty<T>()
let w: T
do {
const popped = List.head(stack)
if (Option.isNone(popped)) break
w = popped.value
stack = Option.getOrElse(List.tail(stack), () => List.empty<T>())
onStack.delete(w)
component = List.prepend(component, w)
} while (!this.equals(w!, v))
if (List.isNonEmpty(component)) {
components = List.prepend(components, component)
}
}
}
HashSet.forEach(this.graph.vertices, (v) => {
if (!index.has(v)) {
strongConnect(v)
}
})
return components
}
}
// Example: Dependency resolution
interface Package {
name: string
version: string
}
const packageEquals: Equal.Equal<Package> = Equal.make((a, b) =>
a.name === b.name && a.version === b.version
)
const buildDependencyGraph = (
packages: List.List<Package>,
dependencies: Map<string, List.List<string>>
): Graph<Package> => {
const vertices = HashSet.fromIterable(packages)
const edges = HashMap.empty<Package>()
return List.reduce(packages, { vertices, edges }, (graph, pkg) => {
const deps = dependencies.get(pkg.name) ?? []
const depPackages = List.fromIterable(deps).pipe(
List.filterMap((depName) =>
List.findFirst(packages, (p) => p.name === depName)
)
)
return {
...graph,
edges: HashMap.set(graph.edges, pkg, depPackages)
}
})
}
// Usage
const packages = List.make(
{ name: "react", version: "18.0.0" },
{ name: "react-dom", version: "18.0.0" },
{ name: "webpack", version: "5.0.0" },
{ name: "babel", version: "7.0.0" }
)
const dependencies = new Map([
["react-dom", ["react"]],
["webpack", ["babel"]],
["babel", []]
])
const depGraph = buildDependencyGraph(packages, dependencies)
const algorithms = new GraphAlgorithms(depGraph, packageEquals)
// Find installation order
Option.match(algorithms.topologicalSort(), {
onNone: () => console.log("Circular dependency detected!"),
onSome: (order) => {
console.log("Installation order:",
pipe(List.map(order, (p) => p.name), List.toArray)
)
}
})Effect's List provides stack-safe operations for recursive algorithms that would overflow with regular recursion:
import { List, pipe } from "effect"
// Stack-safe factorial using List accumulator
const factorial = (n: number): bigint => {
const numbers = List.range(1, n)
return pipe(
List.reduce(1n, (acc, num) => acc * BigInt(num))(numbers)
)
}
// Stack-safe Fibonacci sequence
const fibonacci = (n: number): List.List<number> => {
const go = (
count: number,
current: number,
next: number,
acc: List.List<number>
): List.List<number> => {
if (count === 0) return List.reverse(acc)
return go(
count - 1,
next,
current + next,
List.prepend(acc, current)
)
}
return go(n, 0, 1, List.empty())
}
// Stack-safe tree flattening
interface TreeNode<T> {
value: T
children: List.List<TreeNode<T>>
}
const flattenTree = <T>(tree: TreeNode<T>): List.List<T> => {
// Using a work list pattern for stack safety
const go = (
workList: List.List<TreeNode<T>>,
acc: List.List<T>
): List.List<T> => {
if (List.isEmpty(workList)) return acc
return pipe(
List.head(workList),
Option.match({
onNone: () => acc,
onSome: (node) => {
const remaining = Option.getOrElse(
List.tail(workList),
() => List.empty<TreeNode<T>>()
)
const newWorkList = List.prependAll(remaining, node.children)
return go(newWorkList, List.prepend(acc, node.value))
}
})
)
}
return List.reverse(go(List.of(tree), List.empty()))
}
// Example: Deep tree traversal
const deepTree: TreeNode<number> = {
value: 1,
children: List.make(
{
value: 2,
children: List.make(
{ value: 4, children: List.empty() },
{ value: 5, children: List.empty() }
)
},
{
value: 3,
children: List.make(
{ value: 6, children: List.empty() }
)
}
)
}
console.log("Flattened tree:", List.toArray(flattenTree(deepTree))) // [1, 2, 4, 5, 3, 6]Understanding when to use List vs Array for optimal performance:
import { List, Array, pipe, Effect, Duration } from "effect"
// List excels at prepend-heavy operations
const buildListPrepend = (n: number): Effect.Effect<List.List<number>> =>
Effect.sync(() => {
let list = List.empty<number>()
for (let i = 0; i < n; i++) {
list = List.prepend(list, i) // O(1) per operation
}
return list
})
// Array struggles with prepend
const buildArrayPrepend = (n: number): Effect.Effect<Array<number>> =>
Effect.sync(() => {
let arr: number[] = []
for (let i = 0; i < n; i++) {
arr = [i, ...arr] // O(n) per operation - quadratic overall!
}
return arr
})
// List with proper batching for append operations
const buildListAppendBatched = (n: number): Effect.Effect<List.List<number>> =>
Effect.sync(() => {
const chunks: List.List<number>[] = []
let current = List.empty<number>()
for (let i = 0; i < n; i++) {
current = List.prepend(current, i)
// Batch reversal for efficiency
if (List.length(current) >= 1000) {
chunks.push(List.reverse(current))
current = List.empty()
}
}
if (List.isNonEmpty(current)) {
chunks.push(List.reverse(current))
}
return chunks.reduce(
(acc, chunk) => List.appendAll(acc, chunk),
List.empty<number>()
)
})
// Benchmark utility
const benchmark = <A>(name: string, effect: Effect.Effect<A>) =>
pipe(
Effect.timed(effect),
Effect.map(([duration, _]) => {
console.log(`${name}: ${Duration.toMillis(duration)}ms`)
})
)
// Compare performance
const runBenchmarks = Effect.gen(function* () {
const size = 10000
yield* benchmark("List prepend", buildListPrepend(size))
yield* benchmark("Array prepend", buildArrayPrepend(size))
yield* benchmark("List append (batched)", buildListAppendBatched(size))
})List provides powerful combinators for complex transformations:
import { List, Option, pipe, Predicate } from "effect"
// Sliding window pattern
const slidingWindow = <A>(list: List.List<A>, size: number): List.List<List.List<A>> => {
if (size <= 0 || List.isEmpty(list)) return List.empty()
const go = (
remaining: List.List<A>,
acc: List.List<List.List<A>>
): List.List<List.List<A>> => {
const window = List.take(remaining, size)
if (List.length(window) < size) {
return List.reverse(acc)
}
return pipe(
List.tail(remaining),
Option.match({
onNone: () => List.reverse(List.prepend(acc, window)),
onSome: (tail) => go(tail, List.prepend(acc, window))
})
)
}
return go(list, List.empty())
}
// Group consecutive elements
const groupConsecutive = <A>(
list: List.List<A>,
equals: (a: A, b: A) => boolean
): List.List<List.List<A>> => {
if (List.isEmpty(list)) return List.empty()
return pipe(
List.reduce(
list,
List.empty<List.List<A>>(),
(groups, item) => {
return pipe(
List.head(groups),
Option.match({
onNone: () => List.of(List.of(item)),
onSome: (currentGroup) =>
pipe(
List.head(currentGroup),
Option.match({
onNone: () => List.prepend(groups, List.of(item)),
onSome: (lastItem) =>
equals(lastItem, item)
? List.prepend(
Option.getOrElse(List.tail(groups), () => List.empty()),
List.prepend(currentGroup, item)
)
: List.prepend(groups, List.of(item))
})
)
})
)
}
),
List.map(List.reverse),
List.reverse
)
}
// Partition with multiple predicates
const multiPartition = <A>(
list: List.List<A>,
predicates: ReadonlyArray<Predicate.Predicate<A>>
): ReadonlyArray<List.List<A>> => {
const initial = predicates.map(() => List.empty<A>())
return pipe(
list,
List.reduce(initial, (partitions, item) => {
const index = predicates.findIndex((pred) => pred(item))
if (index === -1) return partitions
return partitions.map((partition, i) =>
i === index ? List.prepend(partition, item) : partition
)
}),
(partitions) => partitions.map(List.reverse)
)
}
// Example usage
const numbers = List.range(1, 20)
// Sliding windows
const windows = slidingWindow(numbers, 3)
console.log("Sliding windows:",
pipe(List.map(windows, List.toArray), List.toArray)
) // [[1,2,3], [2,3,4], [3,4,5], ...]
// Group consecutive
const consecutive = List.make(1, 1, 2, 2, 2, 3, 1, 1)
const groups = groupConsecutive(consecutive, (a, b) => a === b)
console.log("Grouped:",
pipe(List.map(groups, List.toArray), List.toArray)
) // [[1,1], [2,2,2], [3], [1,1]]
// Multi-partition
const [evens, divBy3, divBy5] = multiPartition(
numbers,
[
(n) => n % 2 === 0,
(n) => n % 3 === 0,
(n) => n % 5 === 0
]
)
console.log("Evens:", List.toArray(evens)) // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
console.log("Divisible by 3:", List.toArray(divBy3)) // [3, 6, 9, 12, 15, 18]
console.log("Divisible by 5:", List.toArray(divBy5)) // [5, 10, 15, 20]Effect's List module provides several optimizations for chaining operations:
import { List, pipe, Function as Fn } from "effect"
// Automatic fusion for map operations
const fusedMap = <A, B, C, D>(
list: List.List<A>,
f: (a: A) => B,
g: (b: B) => C,
h: (c: C) => D
): List.List<D> => {
// Instead of multiple passes
// return pipe(list, List.map(f), List.map(g), List.map(h))
// Single pass with composed function
return pipe(
List.map(Fn.flow(f, g, h))(list)
)
}
// Optimized filter + map pattern
const filterMap = <A, B>(
list: List.List<A>,
predicate: (a: A) => boolean,
mapper: (a: A) => B
): List.List<B> => {
return pipe(
List.reduce(List.empty<B>(), (acc, item) =>
predicate(item)
? List.prepend(acc, mapper(item))
: acc
)(list),
List.reverse
)
}
// Optimized take while pattern
const takeWhileWithIndex = <A>(
list: List.List<A>,
predicate: (a: A, index: number) => boolean
): List.List<A> => {
const go = (
remaining: List.List<A>,
index: number,
acc: List.List<A>
): List.List<A> => {
return pipe(
List.head(remaining),
Option.match({
onNone: () => List.reverse(acc),
onSome: (item) => {
if (predicate(item, index)) {
return go(
Option.getOrElse(List.tail(remaining), () => List.empty<A>()),
index + 1,
List.prepend(acc, item)
)
}
return List.reverse(acc)
}
})
)
}
return go(list, 0, List.empty())
}
// Chunk processing for large lists
const chunkProcess = <A, B>(
list: List.List<A>,
chunkSize: number,
processor: (chunk: List.List<A>) => List.List<B>
): List.List<B> => {
const processChunks = (
remaining: List.List<A>,
acc: List.List<B>
): List.List<B> => {
if (List.isEmpty(remaining)) {
return List.reverse(acc)
}
const chunk = List.take(remaining, chunkSize)
const processed = processor(chunk)
const newRemaining = List.drop(remaining, chunkSize)
return processChunks(
newRemaining,
List.prependAll(acc, List.reverse(processed))
)
}
return processChunks(list, List.empty())
}
// Example usage
const largeNumbers = List.range(1, 10000)
// Optimized filtering and mapping
const result = filterMap(
largeNumbers,
(n) => n % 2 === 0,
(n) => n * n
)
// Chunked processing for memory efficiency
const chunks = chunkProcess(
largeNumbers,
1000,
(chunk) => pipe(
chunk,
List.filter((n) => n % 3 === 0),
List.map((n) => n.toString())
)
)import { List, Array as EffectArray, pipe } from "effect"
// Use List when:
// 1. Frequent prepend operations
// 2. Recursive algorithms with deep nesting
// 3. Persistent data structures needed
// 4. Undo/redo or history tracking
// 5. Functional programming patterns (fold, unfold)
// Use Array when:
// 1. Random access is required
// 2. Numeric computations
// 3. Large datasets with index-based operations
// 4. Interop with existing JS APIs
// 5. Memory-critical applications
// Helper to choose the right structure
type CollectionChoice<A> = {
readonly preprendHeavy: List.List<A>
readonly indexHeavy: ReadonlyArray<A>
readonly balanced: ReadonlyArray<A>
}
const chooseCollection = <A>(
operations: {
prepends: number
randomAccess: number
iterations: number
}
): keyof CollectionChoice<A> => {
const { prepends, randomAccess, iterations } = operations
if (prepends > randomAccess * 2) return "preprendHeavy"
if (randomAccess > prepends * 2) return "indexHeavy"
return "balanced"
}
// Conversion utilities for gradual migration
const listToArrayWhenNeeded = <A, B>(
list: List.List<A>,
operation: (arr: ReadonlyArray<A>) => B
): B => {
return operation(List.toArray(list))
}
const arrayToListForOperation = <A, B>(
array: ReadonlyArray<A>,
operation: (list: List.List<A>) => List.List<B>
): ReadonlyArray<B> => {
return pipe(
List.fromIterable(array),
operation,
List.toArray
)
}import { List, Effect, Option, Either, pipe } from "effect"
// Error recovery with partial results
const processWithRecovery = <A, B>(
list: List.List<A>,
processor: (a: A) => Effect.Effect<B, Error>
): Effect.Effect<{ successes: List.List<B>; failures: List.List<{ item: A; error: Error }> }> => {
return Effect.gen(function* () {
let successes = List.empty<B>()
let failures = List.empty<{ item: A; error: Error }>()
for (const item of List.toArray(list)) {
const result = yield* Effect.either(processor(item))
if (Either.isRight(result)) {
successes = List.prepend(successes, result.right)
} else {
failures = List.prepend(failures, { item, error: result.left })
}
}
return {
successes: List.reverse(successes),
failures: List.reverse(failures)
}
})
}
// Circuit breaker pattern with List
class CircuitBreaker<A, B> {
private failures = List.empty<Date>()
private readonly failureThreshold: number
private readonly recoveryTimeout: number
constructor(failureThreshold: number = 5, recoveryTimeout: number = 60000) {
this.failureThreshold = failureThreshold
this.recoveryTimeout = recoveryTimeout
}
process(
items: List.List<A>,
processor: (a: A) => Effect.Effect<B, Error>
): Effect.Effect<List.List<B>, Error> {
return Effect.gen(function* () {
// Clean old failures
const now = new Date()
this.failures = List.filter(
this.failures,
(failureTime) => now.getTime() - failureTime.getTime() < this.recoveryTimeout
)
// Check if circuit is open
if (List.length(this.failures) >= this.failureThreshold) {
yield* Effect.fail(new Error("Circuit breaker is open"))
}
let results = List.empty<B>()
for (const item of List.toArray(items)) {
try {
const result = yield* processor(item)
results = List.prepend(results, result)
} catch (error) {
this.failures = List.prepend(this.failures, now)
if (List.length(this.failures) >= this.failureThreshold) {
yield* Effect.fail(new Error("Circuit breaker opened due to failures"))
}
throw error
}
}
return List.reverse(results)
})
}
}
// Retry pattern with exponential backoff
const retryWithBackoff = <A, B>(
items: List.List<A>,
processor: (a: A) => Effect.Effect<B, Error>,
maxRetries: number = 3
): Effect.Effect<List.List<B>, Error> => {
const processWithRetry = (item: A): Effect.Effect<B, Error> => {
const attempt = (retries: number): Effect.Effect<B, Error> => {
return Effect.catchAll(processor(item), (error) => {
if (retries <= 0) {
return Effect.fail(error)
}
const delay = Math.pow(2, maxRetries - retries) * 1000
return pipe(
Effect.sleep(`${delay} millis`),
Effect.flatMap(() => attempt(retries - 1))
)
})
}
return attempt(maxRetries)
}
return pipe(
items,
List.map(processWithRetry),
(effects) => Effect.all(List.toArray(effects)),
Effect.map(List.fromIterable)
)
}import { List, Effect, Option, pipe } from "effect"
// Lazy list generation to avoid memory spikes
const lazyRange = function* (start: number, end: number) {
for (let i = start; i <= end; i++) {
yield i
}
}
const processLargeDataset = <A, B>(
generator: Generator<A>,
batchSize: number,
process: (batch: List.List<A>) => Effect.Effect<List.List<B>>
): Effect.Effect<List.List<B>> => {
return Effect.gen(function* () {
let results = List.empty<B>()
let batch = List.empty<A>()
let count = 0
for (const item of generator) {
batch = List.prepend(batch, item)
count++
if (count >= batchSize) {
const processed = yield* process(List.reverse(batch))
results = List.appendAll(results, processed)
batch = List.empty()
count = 0
}
}
// Process remaining items
if (List.isNonEmpty(batch)) {
const processed = yield* process(List.reverse(batch))
results = List.appendAll(results, processed)
}
return results
})
}
// Memory-efficient operations using generators
const streamingMap = <A, B>(
list: List.List<A>,
f: (a: A) => B,
chunkSize: number = 1000
): Generator<B> => {
function* go(remaining: List.List<A>): Generator<B> {
const chunk = List.take(remaining, chunkSize)
if (List.isEmpty(chunk)) return
for (const item of List.toArray(chunk)) {
yield f(item)
}
const rest = List.drop(remaining, chunkSize)
if (List.isNonEmpty(rest)) {
yield* go(rest)
}
}
return go(list)
}
// Memory pool for frequent List operations
class ListPool<T> {
private pool = List.empty<List.List<T>>()
private readonly maxSize: number
constructor(maxSize: number = 10) {
this.maxSize = maxSize
}
acquire(): List.List<T> {
return pipe(
List.head(this.pool),
Option.match({
onNone: () => List.empty<T>(),
onSome: (reusable) => {
this.pool = Option.getOrElse(
List.tail(this.pool),
() => List.empty<List.List<T>>()
)
return reusable
}
})
)
}
release(list: List.List<T>): void {
if (List.length(this.pool) < this.maxSize) {
// Clear the list and return to pool
this.pool = List.prepend(this.pool, List.empty<T>())
}
}
withPooled<R>(operation: (list: List.List<T>) => R): R {
const list = this.acquire()
try {
return operation(list)
} finally {
this.release(list)
}
}
}
// Garbage collection friendly operations
const gcFriendlyReduce = <A, B>(
list: List.List<A>,
initial: B,
reducer: (acc: B, item: A) => B,
gcInterval: number = 1000
): B => {
let acc = initial
let count = 0
const processChunk = (chunk: List.List<A>): B => {
return List.reduce(chunk, acc, reducer)
}
let current = list
while (List.isNonEmpty(current)) {
const chunk = List.take(current, gcInterval)
acc = processChunk(chunk)
current = List.drop(current, gcInterval)
count += List.length(chunk)
// Allow GC to run
if (count % (gcInterval * 10) === 0) {
// Force micro-task to allow GC
setTimeout(() => {}, 0)
}
}
return acc
}import { List, Option, Equal, pipe, Array as EffectArray } from "effect"
import * as fc from "fast-check"
// Property-based testing for List operations
const listArbitrary = <A>(arb: fc.Arbitrary<A>): fc.Arbitrary<List.List<A>> =>
fc.array(arb).map(List.fromIterable)
// Test properties
describe("List Laws", () => {
test("prepend/head/tail consistency", () => {
fc.assert(
fc.property(
fc.integer(),
listArbitrary(fc.integer()),
(head, tail) => {
const list = List.prepend(tail, head)
return (
Option.getOrNull(List.head(list)) === head &&
Equal.equals(
Option.getOrElse(List.tail(list), () => List.empty<number>()),
tail
)
)
}
)
)
})
test("reverse involution", () => {
fc.assert(
fc.property(
listArbitrary(fc.integer()),
(list) => Equal.equals(List.reverse(List.reverse(list)), list)
)
)
})
test("map fusion", () => {
fc.assert(
fc.property(
listArbitrary(fc.integer()),
(list) => {
const f = (x: number) => x * 2
const g = (x: number) => x + 1
return Equal.equals(
pipe(List.map(list, f), List.map(g)),
pipe(List.map(list, (x) => g(f(x))))
)
}
)
)
})
})
// Test helpers for async operations
const testListEffect = async <A>(
list: List.List<Effect.Effect<A>>,
expected: List.List<A>
): Promise<void> => {
const results = await Effect.runPromise(
Effect.all(List.toArray(list), { concurrency: "unbounded" })
)
expect(List.fromIterable(results)).toEqual(expected)
}
// Snapshot testing for complex transformations
const snapshotList = <A>(
list: List.List<A>,
name: string
): void => {
const snapshot = {
length: List.length(list),
head: Option.getOrNull(List.head(list)),
last: Option.getOrNull(List.last(list)),
items: List.toArray(List.take(list, 10)), // First 10 for readability
isEmpty: List.isEmpty(list)
}
expect(snapshot).toMatchSnapshot(name)
}import { List, Stream, Effect, pipe, Chunk } from "effect"
// Convert List to Stream for async processing
const listToStream = <A>(list: List.List<A>): Stream.Stream<A> => {
return Stream.fromIterable(List.toArray(list))
}
// Collect Stream into List
const streamToList = <A>(stream: Stream.Stream<A>): Effect.Effect<List.List<A>> => {
return pipe(
stream,
Stream.runCollect,
Effect.map((chunk) => List.fromIterable(Chunk.toArray(chunk)))
)
}
// Process List items as Stream with backpressure
const processListAsync = <A, B>(
list: List.List<A>,
process: (a: A) => Effect.Effect<B>,
concurrency: number = 5
): Effect.Effect<List.List<B>> => {
return pipe(
Stream.mapEffect(process, { concurrency })(listToStream(list)),
streamToList
)
}
// Example: Parallel API calls with rate limiting
interface User {
id: number
name: string
}
interface EnrichedUser extends User {
profile: UserProfile
posts: Post[]
}
const enrichUsers = (users: List.List<User>): Effect.Effect<List.List<EnrichedUser>> => {
const fetchProfile = (userId: number): Effect.Effect<UserProfile> =>
Effect.succeed({ userId, avatar: "avatar.jpg", bio: "Bio text" })
const fetchPosts = (userId: number): Effect.Effect<Post[]> =>
Effect.succeed([{ id: 1, userId, title: "Post 1", content: "Content" }])
return pipe(
Stream.mapEffect(
(user) =>
Effect.all({
profile: fetchProfile(user.id),
posts: fetchPosts(user.id)
}).pipe(
Effect.map(({ profile, posts }) => ({
...user,
profile,
posts
}))
),
{ concurrency: 3 } // Process 3 users at a time
)(listToStream(users)),
Stream.throttle({
cost: 1,
duration: "100 millis",
units: 10
}), // Rate limit to 10 requests per 100ms
streamToList
)
}import { List, Schema, pipe, ParseResult } from "effect"
// Define schemas for List-based data structures
const TaskSchema = Schema.Struct({
id: Schema.String,
title: Schema.String,
completed: Schema.Boolean,
priority: Schema.Literal("low", "medium", "high"),
tags: Schema.Array(Schema.String),
createdAt: Schema.DateFromString,
dueDate: Schema.optional(Schema.DateFromString)
})
const TaskListSchema = Schema.Array(TaskSchema)
// Custom schema for List with validation
const ValidatedTaskListSchema = pipe(
TaskListSchema,
Schema.transform(
Schema.Array(TaskSchema),
Schema.unknown,
{
decode: (array) => {
const list = List.fromIterable(array)
// Custom validation: ensure no duplicate IDs
const ids = List.map(list, (task) => task.id)
const uniqueIds = new Set(List.toArray(ids))
if (uniqueIds.size !== List.length(list)) {
return ParseResult.fail(ParseResult.type("Tasks must have unique IDs"))
}
// Custom validation: high priority tasks must have due dates
const invalidHighPriority = List.findFirst(list, (task) =>
task.priority === "high" && !task.dueDate
)
if (Option.isSome(invalidHighPriority)) {
return ParseResult.fail(
ParseResult.type("High priority tasks must have due dates")
)
}
return ParseResult.succeed(list)
},
encode: (list) => List.toArray(list as List.List<any>)
}
)
)
// Advanced schema transformations with List
const ProjectSchema = Schema.Struct({
id: Schema.String,
name: Schema.String,
tasks: ValidatedTaskListSchema,
teamMembers: Schema.Array(Schema.String)
})
// Schema for serialization (List -> Array)
const ProjectSerializationSchema = pipe(
ProjectSchema,
Schema.transform(
ProjectSchema,
Schema.Struct({
id: Schema.String,
name: Schema.String,
tasks: Schema.Array(TaskSchema),
teamMembers: Schema.Array(Schema.String),
metadata: Schema.Struct({
totalTasks: Schema.Number,
completedTasks: Schema.Number,
highPriorityTasks: Schema.Number
})
}),
{
decode: (serialized) => ({
id: serialized.id,
name: serialized.name,
tasks: List.fromIterable(serialized.tasks),
teamMembers: serialized.teamMembers
}),
encode: (project) => {
const tasks = project.tasks as List.List<typeof TaskSchema.Type>
const completed = List.filter(tasks, (task) => task.completed)
const highPriority = List.filter(tasks, (task) => task.priority === "high")
return {
id: project.id,
name: project.name,
tasks: List.toArray(tasks),
teamMembers: project.teamMembers,
metadata: {
totalTasks: List.length(tasks),
completedTasks: List.length(completed),
highPriorityTasks: List.length(highPriority)
}
}
}
}
)
)
// List-specific schema helpers
const OrderedListSchema = <A>(itemSchema: Schema.Schema<A, A>) =>
pipe(
Schema.Array(itemSchema),
Schema.transform(
Schema.Array(itemSchema),
Schema.unknown,
{
decode: (array) => {
const list = List.fromIterable(array)
// Ensure list maintains insertion order
return ParseResult.succeed(list)
},
encode: (list) => List.toArray(list as List.List<A>)
}
)
)
const SortedListSchema = <A>(
itemSchema: Schema.Schema<A, A>,
compare: (a: A, b: A) => number
) =>
pipe(
Schema.Array(itemSchema),
Schema.transform(
Schema.Array(itemSchema),
Schema.unknown,
{
decode: (array) => {
const sorted = [...array].sort(compare)
return ParseResult.succeed(List.fromIterable(sorted))
},
encode: (list) => List.toArray(list as List.List<A>)
}
)
)
// Usage examples
const validateAndParseProject = (data: unknown) =>
Schema.decodeUnknown(ProjectSchema)(data)
const serializeProject = (project: typeof ProjectSchema.Type) =>
Schema.encode(ProjectSerializationSchema)(project)
// Example with sorted list
const PriorityTaskListSchema = SortedListSchema(
TaskSchema,
(a, b) => {
const priorityOrder = { high: 3, medium: 2, low: 1 }
return priorityOrder[b.priority] - priorityOrder[a.priority]
}
)import { List, Effect, pipe, HttpApi, HttpApiBuilder, HttpApiGroup } from "@effect/platform"
// HTTP API with List-based responses
const TasksApi = HttpApiGroup.make("tasks")
.add(
HttpApi.get("getAllTasks", "/tasks")
.setSuccess(Schema.Array(TaskSchema))
)
.add(
HttpApi.post("createTasks", "/tasks/batch")
.setPayload(Schema.Array(TaskSchema))
.setSuccess(Schema.Struct({
created: Schema.Array(TaskSchema),
errors: Schema.Array(Schema.Struct({
task: TaskSchema,
error: Schema.String
}))
}))
)
.add(
HttpApi.get("getTasksByPriority", "/tasks/priority/:priority")
.setParams(Schema.Struct({ priority: Schema.Literal("low", "medium", "high") }))
.setSuccess(Schema.Array(TaskSchema))
)
// Service implementation using Lists
interface TaskService {
getAllTasks(): Effect.Effect<List.List<Task>>
createTasks(tasks: List.List<Task>): Effect.Effect<{
created: List.List<Task>
errors: List.List<{ task: Task; error: string }>
}>
getTasksByPriority(priority: "low" | "medium" | "high"): Effect.Effect<List.List<Task>>
}
const TaskServiceLive = Effect.gen(function* () {
let tasks = List.empty<Task>()
const getAllTasks = () => Effect.succeed(tasks)
const createTasks = (newTasks: List.List<Task>) =>
Effect.gen(function* () {
let created = List.empty<Task>()
let errors = List.empty<{ task: Task; error: string }>()
for (const task of List.toArray(newTasks)) {
// Validate task
if (!task.title.trim()) {
errors = List.prepend(errors, {
task,
error: "Task title cannot be empty"
})
continue
}
// Check for duplicate ID
const exists = List.some(tasks, (existing) => existing.id === task.id)
if (exists) {
errors = List.prepend(errors, {
task,
error: "Task with this ID already exists"
})
continue
}
// Add task
tasks = List.prepend(tasks, task)
created = List.prepend(created, task)
}
return {
created: List.reverse(created),
errors: List.reverse(errors)
}
})
const getTasksByPriority = (priority: "low" | "medium" | "high") =>
Effect.succeed(
List.filter(tasks, (task) => task.priority === priority)
)
return {
getAllTasks,
createTasks,
getTasksByPriority
} satisfies TaskService
})
// HTTP handlers
const TasksApiLive = HttpApiBuilder.group(TasksApi, "/api", (handlers) =>
handlers
.handle("getAllTasks", () =>
Effect.gen(function* () {
const taskService = yield* TaskService
const tasks = yield* taskService.getAllTasks()
return List.toArray(tasks) // Convert to Array for HTTP response
})
)
.handle("createTasks", ({ payload }) =>
Effect.gen(function* () {
const taskService = yield* TaskService
const taskList = List.fromIterable(payload)
const result = yield* taskService.createTasks(taskList)
return {
created: List.toArray(result.created),
errors: List.toArray(result.errors)
}
})
)
.handle("getTasksByPriority", ({ params }) =>
Effect.gen(function* () {
const taskService = yield* TaskService
const tasks = yield* taskService.getTasksByPriority(params.priority)
return List.toArray(tasks)
})
)
)
// Client usage
const taskClient = HttpApi.client(TasksApi)
const createMultipleTasks = (tasks: List.List<Task>) =>
Effect.gen(function* () {
const result = yield* taskClient.createTasks({
payload: List.toArray(tasks)
})
console.log(`Created ${result.created.length} tasks`)
console.log(`${result.errors.length} errors occurred`)
return {
created: List.fromIterable(result.created),
errors: List.fromIterable(result.errors)
}
})import { List, Effect, TestContext, TestClock, pipe } from "effect"
import { describe, test, expect } from "vitest"
// Test utility for List-based operations
const testListOperation = <A, B>(
name: string,
input: List.List<A>,
operation: (list: List.List<A>) => List.List<B>,
expected: List.List<B>
) => {
test(name, () => {
const result = operation(input)
expect(List.toArray(result)).toEqual(List.toArray(expected))
})
}
// Property-based testing utilities
const genList = <A>(genItem: () => A, maxLength: number = 100): List.List<A> => {
const length = Math.floor(Math.random() * maxLength)
const items = Array.from({ length }, genItem)
return List.fromIterable(items)
}
// Test async List operations
describe("Async List Operations", () => {
test("concurrent processing maintains order", async () => {
const items = List.range(1, 10)
const processWithDelay = (n: number) =>
Effect.delay(
Effect.succeed(n * 2),
`${Math.random() * 100} millis`
)
const result = await Effect.runPromise(
pipe(
List.map(processWithDelay)(items),
(effects) => Effect.all(List.toArray(effects), {
concurrency: "unbounded"
}),
Effect.map(List.fromIterable)
)
)
expect(List.toArray(result)).toEqual([2, 4, 6, 8, 10, 12, 14, 16, 18, 20])
})
test("error handling in List operations", async () => {
const items = List.make(1, 2, 3, 4, 5)
const processWithError = (n: number) =>
n === 3
? Effect.fail("Error at 3")
: Effect.succeed(n * 2)
const result = await Effect.runPromise(
pipe(
List.map(processWithError)(items),
(effects) => Effect.all(List.toArray(effects), {
concurrency: 1,
mode: "either"
}),
Effect.map((results) =>
results
.filter(Either.isRight)
.map((e) => e.right)
),
Effect.map(List.fromIterable)
)
)
expect(List.toArray(result)).toEqual([2, 4, 8, 10])
})
})
// Mock data generators for List
const mockUserList = (count: number): List.List<User> =>
List.fromIterable(
Array.from({ length: count }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
active: Math.random() > 0.3
}))
)
// Performance testing
const benchmarkListOperation = async <A>(
name: string,
list: List.List<A>,
operation: (list: List.List<A>) => unknown
) => {
const start = performance.now()
operation(list)
const end = performance.now()
console.log(`${name}: ${(end - start).toFixed(2)}ms`)
}List provides an immutable, persistent linked list that excels at functional programming patterns, recursive algorithms, and prepend-heavy operations in the Effect ecosystem.
Key benefits:
- O(1) Prepend Operations: Ideal for building collections from head to tail
- Stack-Safe Recursion: Process deeply nested structures without stack overflow
- Persistent Data Structure: Efficient undo/redo and time-travel debugging
- Functional Programming: First-class support for map, filter, fold, and other FP patterns
- Memory Efficiency: Structural sharing reduces memory overhead for immutable operations
List shines when you need efficient prepend operations, recursive algorithms, or persistent data structures, while Array remains the better choice for index-based access and numeric computations.