/** * @experimental * @since 3.18.0 */ import * as Data from "./Data.js" import * as Equal from "./Equal.js" import { dual } from "./Function.js" import * as Hash from "./Hash.js" import type { Inspectable } from "./Inspectable.js" import { format, NodeInspectSymbol } from "./Inspectable.js" import * as Option from "./Option.js" import type { Pipeable } from "./Pipeable.js" import { pipeArguments } from "./Pipeable.js" import type { Mutable } from "./Types.js" /** * Safely get a value from a Map, returning an Option. * Uses explicit key presence check with map.has() for better safety. * @internal */ const getMapSafe = (map: Map, key: K): Option.Option => { if (map.has(key)) { return Option.some(map.get(key)!) } return Option.none() } /** * Unique identifier for Graph instances. * * @since 3.18.0 * @category symbol */ export const TypeId: "~effect/Graph" = "~effect/Graph" as const /** * Type identifier for Graph instances. * * @since 3.18.0 * @category symbol */ export type TypeId = typeof TypeId /** * Node index for node identification using plain numbers. * * @since 3.18.0 * @category models */ export type NodeIndex = number /** * Edge index for edge identification using plain numbers. * * @since 3.18.0 * @category models */ export type EdgeIndex = number /** * Edge data containing source, target, and user data. * * @since 3.18.0 * @category models */ export class Edge extends Data.Class<{ readonly source: NodeIndex readonly target: NodeIndex readonly data: E }> {} /** * Graph type for distinguishing directed and undirected graphs. * * @since 3.18.0 * @category models */ export type Kind = "directed" | "undirected" /** * Graph prototype interface. * * @since 3.18.0 * @category models */ export interface Proto extends Iterable, Equal.Equal, Pipeable, Inspectable { readonly [TypeId]: TypeId readonly nodes: Map readonly edges: Map> readonly adjacency: Map> readonly reverseAdjacency: Map> nextNodeIndex: NodeIndex nextEdgeIndex: EdgeIndex isAcyclic: Option.Option } /** * Immutable graph interface. * * @since 3.18.0 * @category models */ export interface Graph extends Proto { readonly type: T readonly mutable: false } /** * Mutable graph interface. * * @since 3.18.0 * @category models */ export interface MutableGraph extends Proto { readonly type: T readonly mutable: true } /** * Directed graph type alias. * * @since 3.18.0 * @category models */ export type DirectedGraph = Graph /** * Undirected graph type alias. * * @since 3.18.0 * @category models */ export type UndirectedGraph = Graph /** * Mutable directed graph type alias. * * @since 3.18.0 * @category models */ export type MutableDirectedGraph = MutableGraph /** * Mutable undirected graph type alias. * * @since 3.18.0 * @category models */ export type MutableUndirectedGraph = MutableGraph // ============================================================================= // Proto Objects // ============================================================================= /** @internal */ const ProtoGraph = { [TypeId]: TypeId, [Symbol.iterator](this: Graph) { return this.nodes[Symbol.iterator]() }, [NodeInspectSymbol](this: Graph) { return this.toJSON() }, [Equal.symbol](this: Graph, that: Equal.Equal): boolean { if (isGraph(that)) { if ( this.nodes.size !== that.nodes.size || this.edges.size !== that.edges.size || this.type !== that.type ) { return false } // Compare nodes for (const [nodeIndex, nodeData] of this.nodes) { if (!that.nodes.has(nodeIndex)) { return false } const otherNodeData = that.nodes.get(nodeIndex)! if (!Equal.equals(nodeData, otherNodeData)) { return false } } // Compare edges for (const [edgeIndex, edgeData] of this.edges) { if (!that.edges.has(edgeIndex)) { return false } const otherEdge = that.edges.get(edgeIndex)! if (!Equal.equals(edgeData, otherEdge)) { return false } } return true } return false }, [Hash.symbol](this: Graph): number { let hash = Hash.string("Graph") hash = hash ^ Hash.string(this.type) hash = hash ^ Hash.number(this.nodes.size) hash = hash ^ Hash.number(this.edges.size) for (const [nodeIndex, nodeData] of this.nodes) { hash = hash ^ (Hash.hash(nodeIndex) + Hash.hash(nodeData)) } for (const [edgeIndex, edgeData] of this.edges) { hash = hash ^ (Hash.hash(edgeIndex) + Hash.hash(edgeData)) } return hash }, toJSON(this: Graph) { return { _id: "Graph", nodeCount: this.nodes.size, edgeCount: this.edges.size, type: this.type } }, toString(this: Graph) { return format(this) }, pipe() { return pipeArguments(this, arguments) } } // ============================================================================= // Constructors // ============================================================================= /** @internal */ export const isGraph = (u: unknown): u is Graph => typeof u === "object" && u !== null && TypeId in u /** * Creates a directed graph, optionally with initial mutations. * * @example * ```ts * import { Graph } from "effect" * * // Directed graph with initial nodes and edges * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, "A->B") * Graph.addEdge(mutable, b, c, "B->C") * }) * ``` * * @since 3.18.0 * @category constructors */ export const directed = (mutate?: (mutable: MutableDirectedGraph) => void): DirectedGraph => { const graph: Mutable> = Object.create(ProtoGraph) graph.type = "directed" graph.nodes = new Map() graph.edges = new Map() graph.adjacency = new Map() graph.reverseAdjacency = new Map() graph.nextNodeIndex = 0 graph.nextEdgeIndex = 0 graph.isAcyclic = Option.some(true) graph.mutable = false if (mutate) { const mutable = beginMutation(graph as DirectedGraph) mutate(mutable as MutableDirectedGraph) return endMutation(mutable) } return graph } /** * Creates an undirected graph, optionally with initial mutations. * * @example * ```ts * import { Graph } from "effect" * * // Undirected graph with initial nodes and edges * const graph = Graph.undirected((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, "A-B") * Graph.addEdge(mutable, b, c, "B-C") * }) * ``` * * @since 3.18.0 * @category constructors */ export const undirected = (mutate?: (mutable: MutableUndirectedGraph) => void): UndirectedGraph => { const graph: Mutable> = Object.create(ProtoGraph) graph.type = "undirected" graph.nodes = new Map() graph.edges = new Map() graph.adjacency = new Map() graph.reverseAdjacency = new Map() graph.nextNodeIndex = 0 graph.nextEdgeIndex = 0 graph.isAcyclic = Option.some(true) graph.mutable = false if (mutate) { const mutable = beginMutation(graph) mutate(mutable as MutableUndirectedGraph) return endMutation(mutable) } return graph } // ============================================================================= // Scoped Mutable API // ============================================================================= /** * Creates a mutable scope for safe graph mutations by copying the data structure. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed() * const mutable = Graph.beginMutation(graph) * // Now mutable can be safely modified without affecting original graph * ``` * * @since 3.18.0 * @category mutations */ export const beginMutation = ( graph: Graph ): MutableGraph => { // Copy adjacency maps with deep cloned arrays const adjacency = new Map>() const reverseAdjacency = new Map>() for (const [nodeIndex, edges] of graph.adjacency) { adjacency.set(nodeIndex, [...edges]) } for (const [nodeIndex, edges] of graph.reverseAdjacency) { reverseAdjacency.set(nodeIndex, [...edges]) } const mutable: Mutable> = Object.create(ProtoGraph) mutable.type = graph.type mutable.nodes = new Map(graph.nodes) mutable.edges = new Map(graph.edges) mutable.adjacency = adjacency mutable.reverseAdjacency = reverseAdjacency mutable.nextNodeIndex = graph.nextNodeIndex mutable.nextEdgeIndex = graph.nextEdgeIndex mutable.isAcyclic = graph.isAcyclic mutable.mutable = true return mutable } /** * Converts a mutable graph back to an immutable graph, ending the mutation scope. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed() * const mutable = Graph.beginMutation(graph) * // ... perform mutations on mutable ... * const newGraph = Graph.endMutation(mutable) * ``` * * @since 3.18.0 * @category mutations */ export const endMutation = ( mutable: MutableGraph ): Graph => { const graph: Mutable> = Object.create(ProtoGraph) graph.type = mutable.type graph.nodes = new Map(mutable.nodes) graph.edges = new Map(mutable.edges) graph.adjacency = mutable.adjacency graph.reverseAdjacency = mutable.reverseAdjacency graph.nextNodeIndex = mutable.nextNodeIndex graph.nextEdgeIndex = mutable.nextEdgeIndex graph.isAcyclic = mutable.isAcyclic graph.mutable = false return graph } /** * Performs scoped mutations on a graph, automatically managing the mutation lifecycle. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed() * const newGraph = Graph.mutate(graph, (mutable) => { * // Safe mutations go here * // mutable gets automatically converted back to immutable * }) * ``` * * @since 3.18.0 * @category mutations */ export const mutate: { /** * Performs scoped mutations on a graph, automatically managing the mutation lifecycle. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed() * const newGraph = Graph.mutate(graph, (mutable) => { * // Safe mutations go here * // mutable gets automatically converted back to immutable * }) * ``` * * @since 3.18.0 * @category mutations */ (f: (mutable: MutableGraph) => void): (graph: Graph) => Graph /** * Performs scoped mutations on a graph, automatically managing the mutation lifecycle. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed() * const newGraph = Graph.mutate(graph, (mutable) => { * // Safe mutations go here * // mutable gets automatically converted back to immutable * }) * ``` * * @since 3.18.0 * @category mutations */ (graph: Graph, f: (mutable: MutableGraph) => void): Graph } = dual(2, ( graph: Graph, f: (mutable: MutableGraph) => void ): Graph => { const mutable = beginMutation(graph) f(mutable) return endMutation(mutable) }) // ============================================================================= // Basic Node Operations // ============================================================================= /** * Adds a new node to a mutable graph and returns its index. * * @example * ```ts * import { Graph } from "effect" * * const result = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * console.log(nodeA) // NodeIndex with value 0 * console.log(nodeB) // NodeIndex with value 1 * }) * ``` * * @since 3.18.0 * @category mutations */ export const addNode = ( mutable: MutableGraph, data: N ): NodeIndex => { const nodeIndex = mutable.nextNodeIndex // Add node data mutable.nodes.set(nodeIndex, data) // Initialize empty adjacency lists mutable.adjacency.set(nodeIndex, []) mutable.reverseAdjacency.set(nodeIndex, []) // Update graph allocators mutable.nextNodeIndex = mutable.nextNodeIndex + 1 return nodeIndex } /** * Gets the data associated with a node index, if it exists. * * @example * ```ts * import { Graph, Option } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * Graph.addNode(mutable, "Node A") * }) * * const nodeIndex = 0 * const nodeData = Graph.getNode(graph, nodeIndex) * * if (Option.isSome(nodeData)) { * console.log(nodeData.value) // "Node A" * } * ``` * * @since 3.18.0 * @category getters */ export const getNode = ( graph: Graph | MutableGraph, nodeIndex: NodeIndex ): Option.Option => getMapSafe(graph.nodes, nodeIndex) /** * Checks if a node with the given index exists in the graph. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * Graph.addNode(mutable, "Node A") * }) * * const nodeIndex = 0 * const exists = Graph.hasNode(graph, nodeIndex) * console.log(exists) // true * * const nonExistentIndex = 999 * const notExists = Graph.hasNode(graph, nonExistentIndex) * console.log(notExists) // false * ``` * * @since 3.18.0 * @category getters */ export const hasNode = ( graph: Graph | MutableGraph, nodeIndex: NodeIndex ): boolean => graph.nodes.has(nodeIndex) /** * Returns the number of nodes in the graph. * * @example * ```ts * import { Graph } from "effect" * * const emptyGraph = Graph.directed() * console.log(Graph.nodeCount(emptyGraph)) // 0 * * const graphWithNodes = Graph.mutate(emptyGraph, (mutable) => { * Graph.addNode(mutable, "Node A") * Graph.addNode(mutable, "Node B") * Graph.addNode(mutable, "Node C") * }) * * console.log(Graph.nodeCount(graphWithNodes)) // 3 * ``` * * @since 3.18.0 * @category getters */ export const nodeCount = ( graph: Graph | MutableGraph ): number => graph.nodes.size /** * Finds the first node that matches the given predicate. * * @example * ```ts * import { Graph, Option } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * Graph.addNode(mutable, "Node A") * Graph.addNode(mutable, "Node B") * Graph.addNode(mutable, "Node C") * }) * * const result = Graph.findNode(graph, (data) => data.startsWith("Node B")) * console.log(result) // Option.some(1) * * const notFound = Graph.findNode(graph, (data) => data === "Node D") * console.log(notFound) // Option.none() * ``` * * @since 3.18.0 * @category getters */ export const findNode = ( graph: Graph | MutableGraph, predicate: (data: N) => boolean ): Option.Option => { for (const [index, data] of graph.nodes) { if (predicate(data)) { return Option.some(index) } } return Option.none() } /** * Finds all nodes that match the given predicate. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * Graph.addNode(mutable, "Start A") * Graph.addNode(mutable, "Node B") * Graph.addNode(mutable, "Start C") * }) * * const result = Graph.findNodes(graph, (data) => data.startsWith("Start")) * console.log(result) // [0, 2] * * const empty = Graph.findNodes(graph, (data) => data === "Not Found") * console.log(empty) // [] * ``` * * @since 3.18.0 * @category getters */ export const findNodes = ( graph: Graph | MutableGraph, predicate: (data: N) => boolean ): Array => { const results: Array = [] for (const [index, data] of graph.nodes) { if (predicate(data)) { results.push(index) } } return results } /** * Finds the first edge that matches the given predicate. * * @example * ```ts * import { Graph, Option } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * const nodeC = Graph.addNode(mutable, "Node C") * Graph.addEdge(mutable, nodeA, nodeB, 10) * Graph.addEdge(mutable, nodeB, nodeC, 20) * }) * * const result = Graph.findEdge(graph, (data) => data > 15) * console.log(result) // Option.some(1) * * const notFound = Graph.findEdge(graph, (data) => data > 100) * console.log(notFound) // Option.none() * ``` * * @since 3.18.0 * @category getters */ export const findEdge = ( graph: Graph | MutableGraph, predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean ): Option.Option => { for (const [edgeIndex, edgeData] of graph.edges) { if (predicate(edgeData.data, edgeData.source, edgeData.target)) { return Option.some(edgeIndex) } } return Option.none() } /** * Finds all edges that match the given predicate. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * const nodeC = Graph.addNode(mutable, "Node C") * Graph.addEdge(mutable, nodeA, nodeB, 10) * Graph.addEdge(mutable, nodeB, nodeC, 20) * Graph.addEdge(mutable, nodeC, nodeA, 30) * }) * * const result = Graph.findEdges(graph, (data) => data >= 20) * console.log(result) // [1, 2] * * const empty = Graph.findEdges(graph, (data) => data > 100) * console.log(empty) // [] * ``` * * @since 3.18.0 * @category getters */ export const findEdges = ( graph: Graph | MutableGraph, predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean ): Array => { const results: Array = [] for (const [edgeIndex, edgeData] of graph.edges) { if (predicate(edgeData.data, edgeData.source, edgeData.target)) { results.push(edgeIndex) } } return results } /** * Updates a single node's data by applying a transformation function. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * Graph.addNode(mutable, "Node A") * Graph.addNode(mutable, "Node B") * Graph.updateNode(mutable, 0, (data) => data.toUpperCase()) * }) * * const nodeData = Graph.getNode(graph, 0) * console.log(nodeData) // Option.some("NODE A") * ``` * * @since 3.18.0 * @category transformations */ export const updateNode = ( mutable: MutableGraph, index: NodeIndex, f: (data: N) => N ): void => { if (!mutable.nodes.has(index)) { return } const currentData = mutable.nodes.get(index)! const newData = f(currentData) mutable.nodes.set(index, newData) } /** * Updates a single edge's data by applying a transformation function. * * @example * ```ts * import { Graph } from "effect" * * const result = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * const edgeIndex = Graph.addEdge(mutable, nodeA, nodeB, 10) * Graph.updateEdge(mutable, edgeIndex, (data) => data * 2) * }) * * const edgeData = Graph.getEdge(result, 0) * console.log(edgeData) // Option.some({ source: 0, target: 1, data: 20 }) * ``` * * @since 3.18.0 * @category mutations */ export const updateEdge = ( mutable: MutableGraph, edgeIndex: EdgeIndex, f: (data: E) => E ): void => { if (!mutable.edges.has(edgeIndex)) { return } const currentEdge = mutable.edges.get(edgeIndex)! const newData = f(currentEdge.data) mutable.edges.set(edgeIndex, { ...currentEdge, data: newData }) } /** * Creates a new graph with transformed node data using the provided mapping function. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * Graph.addNode(mutable, "node a") * Graph.addNode(mutable, "node b") * Graph.addNode(mutable, "node c") * Graph.mapNodes(mutable, (data) => data.toUpperCase()) * }) * * const nodeData = Graph.getNode(graph, 0) * console.log(nodeData) // Option.some("NODE A") * ``` * * @since 3.18.0 * @category transformations */ export const mapNodes = ( mutable: MutableGraph, f: (data: N) => N ): void => { // Transform existing node data in place for (const [index, data] of mutable.nodes) { const newData = f(data) mutable.nodes.set(index, newData) } } /** * Transforms all edge data in a mutable graph using the provided mapping function. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 10) * Graph.addEdge(mutable, b, c, 20) * Graph.mapEdges(mutable, (data) => data * 2) * }) * * const edgeData = Graph.getEdge(graph, 0) * console.log(edgeData) // Option.some({ source: 0, target: 1, data: 20 }) * ``` * * @since 3.18.0 * @category transformations */ export const mapEdges = ( mutable: MutableGraph, f: (data: E) => E ): void => { // Transform existing edge data in place for (const [index, edgeData] of mutable.edges) { const newData = f(edgeData.data) mutable.edges.set(index, { ...edgeData, data: newData }) } } /** * Reverses all edge directions in a mutable graph by swapping source and target nodes. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 1) // A -> B * Graph.addEdge(mutable, b, c, 2) // B -> C * Graph.reverse(mutable) // Now B -> A, C -> B * }) * * const edge0 = Graph.getEdge(graph, 0) * console.log(edge0) // Option.some({ source: 1, target: 0, data: 1 }) - B -> A * ``` * * @since 3.18.0 * @category transformations */ export const reverse = ( mutable: MutableGraph ): void => { // Reverse all edges by swapping source and target for (const [index, edgeData] of mutable.edges) { mutable.edges.set(index, { source: edgeData.target, target: edgeData.source, data: edgeData.data }) } // Clear and rebuild adjacency lists with reversed directions mutable.adjacency.clear() mutable.reverseAdjacency.clear() // Rebuild adjacency lists with reversed directions for (const [edgeIndex, edgeData] of mutable.edges) { // Add to forward adjacency (source -> target) const sourceEdges = mutable.adjacency.get(edgeData.source) || [] sourceEdges.push(edgeIndex) mutable.adjacency.set(edgeData.source, sourceEdges) // Add to reverse adjacency (target <- source) const targetEdges = mutable.reverseAdjacency.get(edgeData.target) || [] targetEdges.push(edgeIndex) mutable.reverseAdjacency.set(edgeData.target, targetEdges) } // Invalidate cycle flag since edge directions changed mutable.isAcyclic = Option.none() } /** * Filters and optionally transforms nodes in a mutable graph using a predicate function. * Nodes that return Option.none are removed along with all their connected edges. * * @example * ```ts * import { Graph, Option } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "active") * const b = Graph.addNode(mutable, "inactive") * const c = Graph.addNode(mutable, "active") * Graph.addEdge(mutable, a, b, 1) * Graph.addEdge(mutable, b, c, 2) * * // Keep only "active" nodes and transform to uppercase * Graph.filterMapNodes(mutable, (data) => * data === "active" ? Option.some(data.toUpperCase()) : Option.none() * ) * }) * * console.log(Graph.nodeCount(graph)) // 2 (only "active" nodes remain) * ``` * * @since 3.18.0 * @category transformations */ export const filterMapNodes = ( mutable: MutableGraph, f: (data: N) => Option.Option ): void => { const nodesToRemove: Array = [] // First pass: identify nodes to remove and transform data for nodes to keep for (const [index, data] of mutable.nodes) { const result = f(data) if (Option.isSome(result)) { // Transform node data mutable.nodes.set(index, result.value) } else { // Mark for removal nodesToRemove.push(index) } } // Second pass: remove filtered out nodes and their edges for (const nodeIndex of nodesToRemove) { removeNode(mutable, nodeIndex) } } /** * Filters and optionally transforms edges in a mutable graph using a predicate function. * Edges that return Option.none are removed from the graph. * * @example * ```ts * import { Graph, Option } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 5) * Graph.addEdge(mutable, b, c, 15) * Graph.addEdge(mutable, c, a, 25) * * // Keep only edges with weight >= 10 and double their weight * Graph.filterMapEdges(mutable, (data) => * data >= 10 ? Option.some(data * 2) : Option.none() * ) * }) * * console.log(Graph.edgeCount(graph)) // 2 (edges with weight 5 removed) * ``` * * @since 3.18.0 * @category transformations */ export const filterMapEdges = ( mutable: MutableGraph, f: (data: E) => Option.Option ): void => { const edgesToRemove: Array = [] // First pass: identify edges to remove and transform data for edges to keep for (const [index, edgeData] of mutable.edges) { const result = f(edgeData.data) if (Option.isSome(result)) { // Transform edge data mutable.edges.set(index, { ...edgeData, data: result.value }) } else { // Mark for removal edgesToRemove.push(index) } } // Second pass: remove filtered out edges for (const edgeIndex of edgesToRemove) { removeEdge(mutable, edgeIndex) } } /** * Filters nodes by removing those that don't match the predicate. * This function modifies the mutable graph in place. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * Graph.addNode(mutable, "active") * Graph.addNode(mutable, "inactive") * Graph.addNode(mutable, "pending") * Graph.addNode(mutable, "active") * * // Keep only "active" nodes * Graph.filterNodes(mutable, (data) => data === "active") * }) * * console.log(Graph.nodeCount(graph)) // 2 (only "active" nodes remain) * ``` * * @since 3.18.0 * @category transformations */ export const filterNodes = ( mutable: MutableGraph, predicate: (data: N) => boolean ): void => { const nodesToRemove: Array = [] // Identify nodes to remove for (const [index, data] of mutable.nodes) { if (!predicate(data)) { nodesToRemove.push(index) } } // Remove filtered out nodes (this also removes connected edges) for (const nodeIndex of nodesToRemove) { removeNode(mutable, nodeIndex) } } /** * Filters edges by removing those that don't match the predicate. * This function modifies the mutable graph in place. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * * Graph.addEdge(mutable, a, b, 5) * Graph.addEdge(mutable, b, c, 15) * Graph.addEdge(mutable, c, a, 25) * * // Keep only edges with weight >= 10 * Graph.filterEdges(mutable, (data) => data >= 10) * }) * * console.log(Graph.edgeCount(graph)) // 2 (edge with weight 5 removed) * ``` * * @since 3.18.0 * @category transformations */ export const filterEdges = ( mutable: MutableGraph, predicate: (data: E) => boolean ): void => { const edgesToRemove: Array = [] // Identify edges to remove for (const [index, edgeData] of mutable.edges) { if (!predicate(edgeData.data)) { edgesToRemove.push(index) } } // Remove filtered out edges for (const edgeIndex of edgesToRemove) { removeEdge(mutable, edgeIndex) } } // ============================================================================= // Cycle Flag Management (Internal) // ============================================================================= /** @internal */ const invalidateCycleFlagOnRemoval = ( mutable: MutableGraph ): void => { // Only invalidate if the graph had cycles (removing edges/nodes cannot introduce cycles in acyclic graphs) // If already unknown (null) or acyclic (true), no need to change if (Option.isSome(mutable.isAcyclic) && mutable.isAcyclic.value === false) { mutable.isAcyclic = Option.none() } } /** @internal */ const invalidateCycleFlagOnAddition = ( mutable: MutableGraph ): void => { // Only invalidate if the graph was acyclic (adding edges cannot remove cycles from cyclic graphs) // If already unknown (null) or cyclic (false), no need to change if (Option.isSome(mutable.isAcyclic) && mutable.isAcyclic.value === true) { mutable.isAcyclic = Option.none() } } // ============================================================================= // Edge Operations // ============================================================================= /** * Adds a new edge to a mutable graph and returns its index. * * @example * ```ts * import { Graph } from "effect" * * const result = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * const edge = Graph.addEdge(mutable, nodeA, nodeB, 42) * console.log(edge) // EdgeIndex with value 0 * }) * ``` * * @since 3.18.0 * @category mutations */ export const addEdge = ( mutable: MutableGraph, source: NodeIndex, target: NodeIndex, data: E ): EdgeIndex => { // Validate that both nodes exist if (!mutable.nodes.has(source)) { throw new Error(`Source node ${source} does not exist`) } if (!mutable.nodes.has(target)) { throw new Error(`Target node ${target} does not exist`) } const edgeIndex = mutable.nextEdgeIndex // Create edge data const edgeData = new Edge({ source, target, data }) mutable.edges.set(edgeIndex, edgeData) // Update adjacency lists const sourceAdjacency = getMapSafe(mutable.adjacency, source) if (Option.isSome(sourceAdjacency)) { sourceAdjacency.value.push(edgeIndex) } const targetReverseAdjacency = getMapSafe(mutable.reverseAdjacency, target) if (Option.isSome(targetReverseAdjacency)) { targetReverseAdjacency.value.push(edgeIndex) } // For undirected graphs, add reverse connections if (mutable.type === "undirected") { const targetAdjacency = getMapSafe(mutable.adjacency, target) if (Option.isSome(targetAdjacency)) { targetAdjacency.value.push(edgeIndex) } const sourceReverseAdjacency = getMapSafe(mutable.reverseAdjacency, source) if (Option.isSome(sourceReverseAdjacency)) { sourceReverseAdjacency.value.push(edgeIndex) } } // Update allocators mutable.nextEdgeIndex = mutable.nextEdgeIndex + 1 // Only invalidate cycle flag if the graph was acyclic // Adding edges cannot remove cycles from cyclic graphs invalidateCycleFlagOnAddition(mutable) return edgeIndex } /** * Removes a node and all its incident edges from a mutable graph. * * @example * ```ts * import { Graph } from "effect" * * const result = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * Graph.addEdge(mutable, nodeA, nodeB, 42) * * // Remove nodeA and all edges connected to it * Graph.removeNode(mutable, nodeA) * }) * ``` * * @since 3.18.0 * @category mutations */ export const removeNode = ( mutable: MutableGraph, nodeIndex: NodeIndex ): void => { // Check if node exists if (!mutable.nodes.has(nodeIndex)) { return // Node doesn't exist, nothing to remove } // Collect all incident edges for removal const edgesToRemove: Array = [] // Get outgoing edges const outgoingEdges = getMapSafe(mutable.adjacency, nodeIndex) if (Option.isSome(outgoingEdges)) { for (const edge of outgoingEdges.value) { edgesToRemove.push(edge) } } // Get incoming edges const incomingEdges = getMapSafe(mutable.reverseAdjacency, nodeIndex) if (Option.isSome(incomingEdges)) { for (const edge of incomingEdges.value) { edgesToRemove.push(edge) } } // Remove all incident edges for (const edgeIndex of edgesToRemove) { removeEdgeInternal(mutable, edgeIndex) } // Remove the node itself mutable.nodes.delete(nodeIndex) mutable.adjacency.delete(nodeIndex) mutable.reverseAdjacency.delete(nodeIndex) // Only invalidate cycle flag if the graph wasn't already known to be acyclic // Removing nodes cannot introduce cycles in an acyclic graph invalidateCycleFlagOnRemoval(mutable) } /** * Removes an edge from a mutable graph. * * @example * ```ts * import { Graph } from "effect" * * const result = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * const edge = Graph.addEdge(mutable, nodeA, nodeB, 42) * * // Remove the edge * Graph.removeEdge(mutable, edge) * }) * ``` * * @since 3.18.0 * @category mutations */ export const removeEdge = ( mutable: MutableGraph, edgeIndex: EdgeIndex ): void => { const wasRemoved = removeEdgeInternal(mutable, edgeIndex) // Only invalidate cycle flag if an edge was actually removed // and only if the graph wasn't already known to be acyclic if (wasRemoved) { invalidateCycleFlagOnRemoval(mutable) } } /** @internal */ const removeEdgeInternal = ( mutable: MutableGraph, edgeIndex: EdgeIndex ): boolean => { // Get edge data const edge = getMapSafe(mutable.edges, edgeIndex) if (Option.isNone(edge)) { return false // Edge doesn't exist, no mutation occurred } const { source, target } = edge.value // Remove from adjacency lists const sourceAdjacency = getMapSafe(mutable.adjacency, source) if (Option.isSome(sourceAdjacency)) { const index = sourceAdjacency.value.indexOf(edgeIndex) if (index !== -1) { sourceAdjacency.value.splice(index, 1) } } const targetReverseAdjacency = getMapSafe(mutable.reverseAdjacency, target) if (Option.isSome(targetReverseAdjacency)) { const index = targetReverseAdjacency.value.indexOf(edgeIndex) if (index !== -1) { targetReverseAdjacency.value.splice(index, 1) } } // For undirected graphs, remove reverse connections if (mutable.type === "undirected") { const targetAdjacency = getMapSafe(mutable.adjacency, target) if (Option.isSome(targetAdjacency)) { const index = targetAdjacency.value.indexOf(edgeIndex) if (index !== -1) { targetAdjacency.value.splice(index, 1) } } const sourceReverseAdjacency = getMapSafe(mutable.reverseAdjacency, source) if (Option.isSome(sourceReverseAdjacency)) { const index = sourceReverseAdjacency.value.indexOf(edgeIndex) if (index !== -1) { sourceReverseAdjacency.value.splice(index, 1) } } } // Remove edge data mutable.edges.delete(edgeIndex) return true // Edge was successfully removed } // ============================================================================= // Edge Query Operations // ============================================================================= /** * Gets the edge data associated with an edge index, if it exists. * * @example * ```ts * import { Graph, Option } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * Graph.addEdge(mutable, nodeA, nodeB, 42) * }) * * const edgeIndex = 0 * const edgeData = Graph.getEdge(graph, edgeIndex) * * if (Option.isSome(edgeData)) { * console.log(edgeData.value.data) // 42 * console.log(edgeData.value.source) // NodeIndex(0) * console.log(edgeData.value.target) // NodeIndex(1) * } * ``` * * @since 3.18.0 * @category getters */ export const getEdge = ( graph: Graph | MutableGraph, edgeIndex: EdgeIndex ): Option.Option> => getMapSafe(graph.edges, edgeIndex) /** * Checks if an edge exists between two nodes in the graph. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * const nodeC = Graph.addNode(mutable, "Node C") * Graph.addEdge(mutable, nodeA, nodeB, 42) * }) * * const nodeA = 0 * const nodeB = 1 * const nodeC = 2 * * const hasAB = Graph.hasEdge(graph, nodeA, nodeB) * console.log(hasAB) // true * * const hasAC = Graph.hasEdge(graph, nodeA, nodeC) * console.log(hasAC) // false * ``` * * @since 3.18.0 * @category getters */ export const hasEdge = ( graph: Graph | MutableGraph, source: NodeIndex, target: NodeIndex ): boolean => { const adjacencyList = getMapSafe(graph.adjacency, source) if (Option.isNone(adjacencyList)) { return false } // Check if any edge in the adjacency list connects to the target for (const edgeIndex of adjacencyList.value) { const edge = getMapSafe(graph.edges, edgeIndex) if (Option.isSome(edge) && edge.value.target === target) { return true } } return false } /** * Returns the number of edges in the graph. * * @example * ```ts * import { Graph } from "effect" * * const emptyGraph = Graph.directed() * console.log(Graph.edgeCount(emptyGraph)) // 0 * * const graphWithEdges = Graph.mutate(emptyGraph, (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * const nodeC = Graph.addNode(mutable, "Node C") * Graph.addEdge(mutable, nodeA, nodeB, 1) * Graph.addEdge(mutable, nodeB, nodeC, 2) * Graph.addEdge(mutable, nodeC, nodeA, 3) * }) * * console.log(Graph.edgeCount(graphWithEdges)) // 3 * ``` * * @since 3.18.0 * @category getters */ export const edgeCount = ( graph: Graph | MutableGraph ): number => graph.edges.size /** * Returns the neighboring nodes (targets of outgoing edges) for a given node. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * const nodeC = Graph.addNode(mutable, "Node C") * Graph.addEdge(mutable, nodeA, nodeB, 1) * Graph.addEdge(mutable, nodeA, nodeC, 2) * }) * * const nodeA = 0 * const nodeB = 1 * const nodeC = 2 * * const neighborsA = Graph.neighbors(graph, nodeA) * console.log(neighborsA) // [NodeIndex(1), NodeIndex(2)] * * const neighborsB = Graph.neighbors(graph, nodeB) * console.log(neighborsB) // [] * ``` * * @since 3.18.0 * @category getters */ export const neighbors = ( graph: Graph | MutableGraph, nodeIndex: NodeIndex ): Array => { const adjacencyList = getMapSafe(graph.adjacency, nodeIndex) if (Option.isNone(adjacencyList)) { return [] } const result: Array = [] for (const edgeIndex of adjacencyList.value) { const edge = getMapSafe(graph.edges, edgeIndex) if (Option.isSome(edge)) { result.push(edge.value.target) } } return result } /** * Get neighbors of a node in a specific direction for bidirectional traversal. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, "A->B") * }) * * const nodeA = 0 * const nodeB = 1 * * // Get outgoing neighbors (nodes that nodeA points to) * const outgoing = Graph.neighborsDirected(graph, nodeA, "outgoing") * * // Get incoming neighbors (nodes that point to nodeB) * const incoming = Graph.neighborsDirected(graph, nodeB, "incoming") * ``` * * @since 3.18.0 * @category queries */ export const neighborsDirected = ( graph: Graph | MutableGraph, nodeIndex: NodeIndex, direction: Direction ): Array => { const adjacencyMap = direction === "incoming" ? graph.reverseAdjacency : graph.adjacency const adjacencyList = getMapSafe(adjacencyMap, nodeIndex) if (Option.isNone(adjacencyList)) { return [] } const result: Array = [] for (const edgeIndex of adjacencyList.value) { const edge = getMapSafe(graph.edges, edgeIndex) if (Option.isSome(edge)) { // For incoming direction, we want the source node instead of target const neighborNode = direction === "incoming" ? edge.value.source : edge.value.target result.push(neighborNode) } } return result } // ============================================================================= // GraphViz Export // ============================================================================= /** * Exports a graph to GraphViz DOT format for visualization. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") * const nodeB = Graph.addNode(mutable, "Node B") * const nodeC = Graph.addNode(mutable, "Node C") * Graph.addEdge(mutable, nodeA, nodeB, 1) * Graph.addEdge(mutable, nodeB, nodeC, 2) * Graph.addEdge(mutable, nodeC, nodeA, 3) * }) * * const dot = Graph.toGraphViz(graph) * console.log(dot) * // digraph G { * // "0" [label="Node A"]; * // "1" [label="Node B"]; * // "2" [label="Node C"]; * // "0" -> "1" [label="1"]; * // "1" -> "2" [label="2"]; * // "2" -> "0" [label="3"]; * // } * ``` * * @since 3.18.0 * @category utils */ export const toGraphViz = ( graph: Graph | MutableGraph, options?: { readonly nodeLabel?: (data: N) => string readonly edgeLabel?: (data: E) => string readonly graphName?: string } ): string => { const { edgeLabel = (data: E) => String(data), graphName = "G", nodeLabel = (data: N) => String(data) } = options ?? {} const isDirected = graph.type === "directed" const graphType = isDirected ? "digraph" : "graph" const edgeOperator = isDirected ? "->" : "--" const lines: Array = [] lines.push(`${graphType} ${graphName} {`) // Add nodes for (const [nodeIndex, nodeData] of graph.nodes) { const label = nodeLabel(nodeData).replace(/"/g, "\\\"") lines.push(` "${nodeIndex}" [label="${label}"];`) } // Add edges for (const [, edgeData] of graph.edges) { const label = edgeLabel(edgeData.data).replace(/"/g, "\\\"") lines.push(` "${edgeData.source}" ${edgeOperator} "${edgeData.target}" [label="${label}"];`) } lines.push("}") return lines.join("\n") } // ============================================================================= // Direction Types for Bidirectional Traversal // ============================================================================= /** * Direction for graph traversal, indicating which edges to follow. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, "A->B") * }) * * // Follow outgoing edges (normal direction) * const outgoingNodes = Array.from(Graph.indices(Graph.dfs(graph, { startNodes: [0], direction: "outgoing" }))) * * // Follow incoming edges (reverse direction) * const incomingNodes = Array.from(Graph.indices(Graph.dfs(graph, { startNodes: [1], direction: "incoming" }))) * ``` * * @since 3.18.0 * @category models */ export type Direction = "outgoing" | "incoming" // ============================================================================= // ============================================================================= // Graph Structure Analysis Algorithms (Phase 5A) // ============================================================================= /** * Checks if the graph is acyclic (contains no cycles). * * Uses depth-first search to detect back edges, which indicate cycles. * For directed graphs, any back edge creates a cycle. For undirected graphs, * a back edge that doesn't go to the immediate parent creates a cycle. * * @example * ```ts * import { Graph } from "effect" * * // Acyclic directed graph (DAG) * const dag = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, "A->B") * Graph.addEdge(mutable, b, c, "B->C") * }) * console.log(Graph.isAcyclic(dag)) // true * * // Cyclic directed graph * const cyclic = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, "A->B") * Graph.addEdge(mutable, b, a, "B->A") // Creates cycle * }) * console.log(Graph.isAcyclic(cyclic)) // false * ``` * * @since 3.18.0 * @category algorithms */ export const isAcyclic = ( graph: Graph | MutableGraph ): boolean => { // Use existing cycle flag if available if (Option.isSome(graph.isAcyclic)) { return graph.isAcyclic.value } // Stack-safe DFS cycle detection using iterative approach const visited = new Set() const recursionStack = new Set() // Stack entry: [node, neighbors, neighborIndex, isFirstVisit] type DfsStackEntry = [NodeIndex, Array, number, boolean] // Get all nodes to handle disconnected components for (const startNode of graph.nodes.keys()) { if (visited.has(startNode)) { continue // Already processed this component } // Iterative DFS with explicit stack const stack: Array = [[startNode, [], 0, true]] while (stack.length > 0) { const [node, neighbors, neighborIndex, isFirstVisit] = stack[stack.length - 1] // First visit to this node if (isFirstVisit) { if (recursionStack.has(node)) { // Back edge found - cycle detected graph.isAcyclic = Option.some(false) return false } if (visited.has(node)) { stack.pop() continue } visited.add(node) recursionStack.add(node) // Get neighbors for this node const nodeNeighbors = Array.from(neighborsDirected(graph, node, "outgoing")) stack[stack.length - 1] = [node, nodeNeighbors, 0, false] continue } // Process next neighbor if (neighborIndex < neighbors.length) { const neighbor = neighbors[neighborIndex] stack[stack.length - 1] = [node, neighbors, neighborIndex + 1, false] if (recursionStack.has(neighbor)) { // Back edge found - cycle detected graph.isAcyclic = Option.some(false) return false } if (!visited.has(neighbor)) { stack.push([neighbor, [], 0, true]) } } else { // Done with this node - backtrack recursionStack.delete(node) stack.pop() } } } // Cache the result graph.isAcyclic = Option.some(true) return true } /** * Checks if an undirected graph is bipartite. * * A bipartite graph is one whose vertices can be divided into two disjoint sets * such that no two vertices within the same set are adjacent. Uses BFS coloring * to determine bipartiteness. * * @example * ```ts * import { Graph } from "effect" * * // Bipartite graph (alternating coloring possible) * const bipartite = Graph.undirected((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * const d = Graph.addNode(mutable, "D") * Graph.addEdge(mutable, a, b, "edge") // Set 1: {A, C}, Set 2: {B, D} * Graph.addEdge(mutable, b, c, "edge") * Graph.addEdge(mutable, c, d, "edge") * }) * console.log(Graph.isBipartite(bipartite)) // true * * // Non-bipartite graph (odd cycle) * const triangle = Graph.undirected((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, "edge") * Graph.addEdge(mutable, b, c, "edge") * Graph.addEdge(mutable, c, a, "edge") // Triangle (3-cycle) * }) * console.log(Graph.isBipartite(triangle)) // false * ``` * * @since 3.18.0 * @category algorithms */ export const isBipartite = ( graph: Graph | MutableGraph ): boolean => { const coloring = new Map() const discovered = new Set() let isBipartiteGraph = true // Get all nodes to handle disconnected components for (const startNode of graph.nodes.keys()) { if (!discovered.has(startNode)) { // Start BFS coloring from this component const queue: Array = [startNode] coloring.set(startNode, 0) // Color start node with 0 discovered.add(startNode) while (queue.length > 0 && isBipartiteGraph) { const current = queue.shift()! const currentColor = coloring.get(current)! const neighborColor: 0 | 1 = currentColor === 0 ? 1 : 0 // Get all neighbors for undirected graph const nodeNeighbors = getUndirectedNeighbors(graph, current) for (const neighbor of nodeNeighbors) { if (!discovered.has(neighbor)) { // Color unvisited neighbor with opposite color coloring.set(neighbor, neighborColor) discovered.add(neighbor) queue.push(neighbor) } else { // Check if neighbor has the same color (conflict) if (coloring.get(neighbor) === currentColor) { isBipartiteGraph = false break } } } } // Early exit if not bipartite if (!isBipartiteGraph) { break } } } return isBipartiteGraph } /** * Get neighbors for undirected graphs by checking both adjacency and reverse adjacency. * For undirected graphs, we need to find the other endpoint of each edge incident to the node. */ const getUndirectedNeighbors = ( graph: Graph | MutableGraph, nodeIndex: NodeIndex ): Array => { const neighbors = new Set() // Check edges where this node is the source const adjacencyList = getMapSafe(graph.adjacency, nodeIndex) if (Option.isSome(adjacencyList)) { for (const edgeIndex of adjacencyList.value) { const edge = getMapSafe(graph.edges, edgeIndex) if (Option.isSome(edge)) { // For undirected graphs, the neighbor is the other endpoint const otherNode = edge.value.source === nodeIndex ? edge.value.target : edge.value.source neighbors.add(otherNode) } } } return Array.from(neighbors) } /** * Find connected components in an undirected graph. * Each component is represented as an array of node indices. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.undirected((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * const d = Graph.addNode(mutable, "D") * Graph.addEdge(mutable, a, b, "edge") // Component 1: A-B * Graph.addEdge(mutable, c, d, "edge") // Component 2: C-D * }) * * const components = Graph.connectedComponents(graph) * console.log(components) // [[0, 1], [2, 3]] * ``` * * @since 3.18.0 * @category algorithms */ export const connectedComponents = ( graph: Graph | MutableGraph ): Array> => { const visited = new Set() const components: Array> = [] for (const startNode of graph.nodes.keys()) { if (!visited.has(startNode)) { // DFS to find all nodes in this component const component: Array = [] const stack: Array = [startNode] while (stack.length > 0) { const current = stack.pop()! if (!visited.has(current)) { visited.add(current) component.push(current) // Add all unvisited neighbors to stack const nodeNeighbors = getUndirectedNeighbors(graph, current) for (const neighbor of nodeNeighbors) { if (!visited.has(neighbor)) { stack.push(neighbor) } } } } components.push(component) } } return components } /** * Find strongly connected components in a directed graph using Kosaraju's algorithm. * Each SCC is represented as an array of node indices. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, "A->B") * Graph.addEdge(mutable, b, c, "B->C") * Graph.addEdge(mutable, c, a, "C->A") // Creates SCC: A-B-C * }) * * const sccs = Graph.stronglyConnectedComponents(graph) * console.log(sccs) // [[0, 1, 2]] * ``` * * @since 3.18.0 * @category algorithms */ export const stronglyConnectedComponents = ( graph: Graph | MutableGraph ): Array> => { const visited = new Set() const finishOrder: Array = [] // Iterate directly over node keys // Step 1: Stack-safe DFS on original graph to get finish times // Stack entry: [node, neighbors, neighborIndex, isFirstVisit] type DfsStackEntry = [NodeIndex, Array, number, boolean] for (const startNode of graph.nodes.keys()) { if (visited.has(startNode)) { continue } const stack: Array = [[startNode, [], 0, true]] while (stack.length > 0) { const [node, nodeNeighbors, neighborIndex, isFirstVisit] = stack[stack.length - 1] if (isFirstVisit) { if (visited.has(node)) { stack.pop() continue } visited.add(node) const nodeNeighborsList = neighbors(graph, node) stack[stack.length - 1] = [node, nodeNeighborsList, 0, false] continue } // Process next neighbor if (neighborIndex < nodeNeighbors.length) { const neighbor = nodeNeighbors[neighborIndex] stack[stack.length - 1] = [node, nodeNeighbors, neighborIndex + 1, false] if (!visited.has(neighbor)) { stack.push([neighbor, [], 0, true]) } } else { // Done with this node - add to finish order (post-order) finishOrder.push(node) stack.pop() } } } // Step 2: Stack-safe DFS on transpose graph in reverse finish order visited.clear() const sccs: Array> = [] for (let i = finishOrder.length - 1; i >= 0; i--) { const startNode = finishOrder[i] if (visited.has(startNode)) { continue } const scc: Array = [] const stack: Array = [startNode] while (stack.length > 0) { const node = stack.pop()! if (visited.has(node)) { continue } visited.add(node) scc.push(node) // Use reverse adjacency (transpose graph) const reverseAdjacency = getMapSafe(graph.reverseAdjacency, node) if (Option.isSome(reverseAdjacency)) { for (const edgeIndex of reverseAdjacency.value) { const edge = getMapSafe(graph.edges, edgeIndex) if (Option.isSome(edge)) { const predecessor = edge.value.source if (!visited.has(predecessor)) { stack.push(predecessor) } } } } } sccs.push(scc) } return sccs } // ============================================================================= // Path Finding Algorithms (Phase 5B) // ============================================================================= /** * Result of a shortest path computation containing the path and total distance. * * @since 3.18.0 * @category models */ export interface PathResult { readonly path: Array readonly distance: number readonly edgeWeights: Array } /** * Find the shortest path between two nodes using Dijkstra's algorithm. * * Dijkstra's algorithm works with non-negative edge weights and finds the shortest * path from a source node to a target node in O((V + E) log V) time complexity. * * @example * ```ts * import { Graph, Option } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 5) * Graph.addEdge(mutable, a, c, 10) * Graph.addEdge(mutable, b, c, 2) * }) * * const result = Graph.dijkstra(graph, 0, 2, (edgeData) => edgeData) * if (Option.isSome(result)) { * console.log(result.value.path) // [0, 1, 2] - shortest path A->B->C * console.log(result.value.distance) // 7 - total distance * } * ``` * * @since 3.18.0 * @category algorithms */ export const dijkstra = ( graph: Graph | MutableGraph, source: NodeIndex, target: NodeIndex, edgeWeight: (edgeData: E) => number ): Option.Option> => { // Validate that source and target nodes exist if (!graph.nodes.has(source)) { throw new Error(`Source node ${source} does not exist`) } if (!graph.nodes.has(target)) { throw new Error(`Target node ${target} does not exist`) } // Early return if source equals target if (source === target) { return Option.some({ path: [source], distance: 0, edgeWeights: [] }) } // Distance tracking and priority queue simulation const distances = new Map() const previous = new Map() const visited = new Set() // Initialize distances // Iterate directly over node keys for (const node of graph.nodes.keys()) { distances.set(node, node === source ? 0 : Infinity) previous.set(node, null) } // Simple priority queue using array (can be optimized with proper heap) const priorityQueue: Array<{ node: NodeIndex; distance: number }> = [ { node: source, distance: 0 } ] while (priorityQueue.length > 0) { // Find minimum distance node (priority queue extract-min) let minIndex = 0 for (let i = 1; i < priorityQueue.length; i++) { if (priorityQueue[i].distance < priorityQueue[minIndex].distance) { minIndex = i } } const current = priorityQueue.splice(minIndex, 1)[0] const currentNode = current.node // Skip if already visited (can happen with duplicate entries) if (visited.has(currentNode)) { continue } visited.add(currentNode) // Early termination if we reached the target if (currentNode === target) { break } // Get current distance const currentDistance = distances.get(currentNode)! // Examine all outgoing edges const adjacencyList = getMapSafe(graph.adjacency, currentNode) if (Option.isSome(adjacencyList)) { for (const edgeIndex of adjacencyList.value) { const edge = getMapSafe(graph.edges, edgeIndex) if (Option.isSome(edge)) { const neighbor = edge.value.target const weight = edgeWeight(edge.value.data) // Validate non-negative weights if (weight < 0) { throw new Error(`Dijkstra's algorithm requires non-negative edge weights, found ${weight}`) } const newDistance = currentDistance + weight const neighborDistance = distances.get(neighbor)! // Relaxation step if (newDistance < neighborDistance) { distances.set(neighbor, newDistance) previous.set(neighbor, { node: currentNode, edgeData: edge.value.data }) // Add to priority queue if not visited if (!visited.has(neighbor)) { priorityQueue.push({ node: neighbor, distance: newDistance }) } } } } } } // Check if target is reachable const targetDistance = distances.get(target)! if (targetDistance === Infinity) { return Option.none() // No path exists } // Reconstruct path const path: Array = [] const edgeWeights: Array = [] let currentNode: NodeIndex | null = target while (currentNode !== null) { path.unshift(currentNode) const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)! if (prev !== null) { edgeWeights.unshift(prev.edgeData) currentNode = prev.node } else { currentNode = null } } return Option.some({ path, distance: targetDistance, edgeWeights }) } /** * Result of all-pairs shortest path computation. * * @since 3.18.0 * @category models */ export interface AllPairsResult { readonly distances: Map> readonly paths: Map | null>> readonly edgeWeights: Map>> } /** * Find shortest paths between all pairs of nodes using Floyd-Warshall algorithm. * * Floyd-Warshall algorithm computes shortest paths between all pairs of nodes in O(V³) time. * It can handle negative edge weights and detect negative cycles. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 3) * Graph.addEdge(mutable, b, c, 2) * Graph.addEdge(mutable, a, c, 7) * }) * * const result = Graph.floydWarshall(graph, (edgeData) => edgeData) * const distanceAToC = result.distances.get(0)?.get(2) // 5 (A->B->C) * const pathAToC = result.paths.get(0)?.get(2) // [0, 1, 2] * ``` * * @since 3.18.0 * @category algorithms */ export const floydWarshall = ( graph: Graph | MutableGraph, edgeWeight: (edgeData: E) => number ): AllPairsResult => { // Get all nodes for Floyd-Warshall algorithm (needs array for nested iteration) const allNodes = Array.from(graph.nodes.keys()) // Initialize distance matrix const dist = new Map>() const next = new Map>() const edgeMatrix = new Map>() // Initialize with infinity for all pairs for (const i of allNodes) { dist.set(i, new Map()) next.set(i, new Map()) edgeMatrix.set(i, new Map()) for (const j of allNodes) { dist.get(i)!.set(j, i === j ? 0 : Infinity) next.get(i)!.set(j, null) edgeMatrix.get(i)!.set(j, null) } } // Set edge weights for (const [, edgeData] of graph.edges) { const weight = edgeWeight(edgeData.data) const i = edgeData.source const j = edgeData.target // Use minimum weight if multiple edges exist const currentWeight = dist.get(i)!.get(j)! if (weight < currentWeight) { dist.get(i)!.set(j, weight) next.get(i)!.set(j, j) edgeMatrix.get(i)!.set(j, edgeData.data) } } // Floyd-Warshall main loop for (const k of allNodes) { for (const i of allNodes) { for (const j of allNodes) { const distIK = dist.get(i)!.get(k)! const distKJ = dist.get(k)!.get(j)! const distIJ = dist.get(i)!.get(j)! if (distIK !== Infinity && distKJ !== Infinity && distIK + distKJ < distIJ) { dist.get(i)!.set(j, distIK + distKJ) next.get(i)!.set(j, next.get(i)!.get(k)!) } } } } // Check for negative cycles for (const i of allNodes) { if (dist.get(i)!.get(i)! < 0) { throw new Error(`Negative cycle detected involving node ${i}`) } } // Build result paths and edge weights const paths = new Map | null>>() const resultEdgeWeights = new Map>>() for (const i of allNodes) { paths.set(i, new Map()) resultEdgeWeights.set(i, new Map()) for (const j of allNodes) { if (i === j) { paths.get(i)!.set(j, [i]) resultEdgeWeights.get(i)!.set(j, []) } else if (dist.get(i)!.get(j)! === Infinity) { paths.get(i)!.set(j, null) resultEdgeWeights.get(i)!.set(j, []) } else { // Reconstruct path iteratively const path: Array = [] const weights: Array = [] let current = i path.push(current) while (current !== j) { const nextNode = next.get(current)!.get(j)! if (nextNode === null) break const edgeData = edgeMatrix.get(current)!.get(nextNode)! if (edgeData !== null) { weights.push(edgeData) } current = nextNode path.push(current) } paths.get(i)!.set(j, path) resultEdgeWeights.get(i)!.set(j, weights) } } } return { distances: dist, paths, edgeWeights: resultEdgeWeights } } /** * Find the shortest path between two nodes using A* pathfinding algorithm. * * A* is an extension of Dijkstra's algorithm that uses a heuristic function to guide * the search towards the target, potentially finding paths faster than Dijkstra's. * The heuristic must be admissible (never overestimate the actual cost). * * @example * ```ts * import { Graph, Option } from "effect" * * const graph = Graph.directed<{x: number, y: number}, number>((mutable) => { * const a = Graph.addNode(mutable, {x: 0, y: 0}) * const b = Graph.addNode(mutable, {x: 1, y: 0}) * const c = Graph.addNode(mutable, {x: 2, y: 0}) * Graph.addEdge(mutable, a, b, 1) * Graph.addEdge(mutable, b, c, 1) * }) * * // Manhattan distance heuristic * const heuristic = (nodeData: {x: number, y: number}, targetData: {x: number, y: number}) => * Math.abs(nodeData.x - targetData.x) + Math.abs(nodeData.y - targetData.y) * * const result = Graph.astar(graph, 0, 2, (edgeData) => edgeData, heuristic) * if (Option.isSome(result)) { * console.log(result.value.path) // [0, 1, 2] - shortest path * console.log(result.value.distance) // 2 - total distance * } * ``` * * @since 3.18.0 * @category algorithms */ export const astar = ( graph: Graph | MutableGraph, source: NodeIndex, target: NodeIndex, edgeWeight: (edgeData: E) => number, heuristic: (sourceNodeData: N, targetNodeData: N) => number ): Option.Option> => { // Validate that source and target nodes exist if (!graph.nodes.has(source)) { throw new Error(`Source node ${source} does not exist`) } if (!graph.nodes.has(target)) { throw new Error(`Target node ${target} does not exist`) } // Early return if source equals target if (source === target) { return Option.some({ path: [source], distance: 0, edgeWeights: [] }) } // Get target node data for heuristic calculations const targetNodeData = getMapSafe(graph.nodes, target) if (Option.isNone(targetNodeData)) { throw new Error(`Target node ${target} data not found`) } // Distance tracking (g-score) and f-score (g + h) const gScore = new Map() const fScore = new Map() const previous = new Map() const visited = new Set() // Initialize scores // Iterate directly over node keys for (const node of graph.nodes.keys()) { gScore.set(node, node === source ? 0 : Infinity) fScore.set(node, Infinity) previous.set(node, null) } // Calculate initial f-score for source const sourceNodeData = getMapSafe(graph.nodes, source) if (Option.isSome(sourceNodeData)) { const h = heuristic(sourceNodeData.value, targetNodeData.value) fScore.set(source, h) } // Priority queue using f-score (total estimated cost) const openSet: Array<{ node: NodeIndex; fScore: number }> = [ { node: source, fScore: fScore.get(source)! } ] while (openSet.length > 0) { // Find node with lowest f-score let minIndex = 0 for (let i = 1; i < openSet.length; i++) { if (openSet[i].fScore < openSet[minIndex].fScore) { minIndex = i } } const current = openSet.splice(minIndex, 1)[0] const currentNode = current.node // Skip if already visited if (visited.has(currentNode)) { continue } visited.add(currentNode) // Early termination if we reached the target if (currentNode === target) { break } // Get current g-score const currentGScore = gScore.get(currentNode)! // Examine all outgoing edges const adjacencyList = getMapSafe(graph.adjacency, currentNode) if (Option.isSome(adjacencyList)) { for (const edgeIndex of adjacencyList.value) { const edge = getMapSafe(graph.edges, edgeIndex) if (Option.isSome(edge)) { const neighbor = edge.value.target const weight = edgeWeight(edge.value.data) // Validate non-negative weights if (weight < 0) { throw new Error(`A* algorithm requires non-negative edge weights, found ${weight}`) } const tentativeGScore = currentGScore + weight const neighborGScore = gScore.get(neighbor)! // If this path to neighbor is better than any previous one if (tentativeGScore < neighborGScore) { // Update g-score and previous gScore.set(neighbor, tentativeGScore) previous.set(neighbor, { node: currentNode, edgeData: edge.value.data }) // Calculate f-score using heuristic const neighborNodeData = getMapSafe(graph.nodes, neighbor) if (Option.isSome(neighborNodeData)) { const h = heuristic(neighborNodeData.value, targetNodeData.value) const f = tentativeGScore + h fScore.set(neighbor, f) // Add to open set if not visited if (!visited.has(neighbor)) { openSet.push({ node: neighbor, fScore: f }) } } } } } } } // Check if target is reachable const targetGScore = gScore.get(target)! if (targetGScore === Infinity) { return Option.none() // No path exists } // Reconstruct path const path: Array = [] const edgeWeights: Array = [] let currentNode: NodeIndex | null = target while (currentNode !== null) { path.unshift(currentNode) const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)! if (prev !== null) { edgeWeights.unshift(prev.edgeData) currentNode = prev.node } else { currentNode = null } } return Option.some({ path, distance: targetGScore, edgeWeights }) } /** * Find the shortest path between two nodes using Bellman-Ford algorithm. * * Bellman-Ford algorithm can handle negative edge weights and detects negative cycles. * It has O(VE) time complexity, slower than Dijkstra's but more versatile. * Returns Option.none() if a negative cycle is detected that affects the path. * * @example * ```ts * import { Graph, Option } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, -1) // Negative weight allowed * Graph.addEdge(mutable, b, c, 3) * Graph.addEdge(mutable, a, c, 5) * }) * * const result = Graph.bellmanFord(graph, 0, 2, (edgeData) => edgeData) * if (Option.isSome(result)) { * console.log(result.value.path) // [0, 1, 2] - shortest path A->B->C * console.log(result.value.distance) // 2 - total distance * } * ``` * * @since 3.18.0 * @category algorithms */ export const bellmanFord = ( graph: Graph | MutableGraph, source: NodeIndex, target: NodeIndex, edgeWeight: (edgeData: E) => number ): Option.Option> => { // Validate that source and target nodes exist if (!graph.nodes.has(source)) { throw new Error(`Source node ${source} does not exist`) } if (!graph.nodes.has(target)) { throw new Error(`Target node ${target} does not exist`) } // Early return if source equals target if (source === target) { return Option.some({ path: [source], distance: 0, edgeWeights: [] }) } // Initialize distances and predecessors const distances = new Map() const previous = new Map() // Iterate directly over node keys for (const node of graph.nodes.keys()) { distances.set(node, node === source ? 0 : Infinity) previous.set(node, null) } // Collect all edges for relaxation const edges: Array<{ source: NodeIndex; target: NodeIndex; weight: number; edgeData: E }> = [] for (const [, edgeData] of graph.edges) { const weight = edgeWeight(edgeData.data) edges.push({ source: edgeData.source, target: edgeData.target, weight, edgeData: edgeData.data }) } // Relax edges up to V-1 times const nodeCount = graph.nodes.size for (let i = 0; i < nodeCount - 1; i++) { let hasUpdate = false for (const edge of edges) { const sourceDistance = distances.get(edge.source)! const targetDistance = distances.get(edge.target)! // Relaxation step if (sourceDistance !== Infinity && sourceDistance + edge.weight < targetDistance) { distances.set(edge.target, sourceDistance + edge.weight) previous.set(edge.target, { node: edge.source, edgeData: edge.edgeData }) hasUpdate = true } } // Early termination if no updates if (!hasUpdate) { break } } // Check for negative cycles for (const edge of edges) { const sourceDistance = distances.get(edge.source)! const targetDistance = distances.get(edge.target)! if (sourceDistance !== Infinity && sourceDistance + edge.weight < targetDistance) { // Negative cycle detected - check if it affects the path to target const affectedNodes = new Set() const queue = [edge.target] while (queue.length > 0) { const node = queue.shift()! if (affectedNodes.has(node)) continue affectedNodes.add(node) // Add all nodes reachable from this node const adjacencyList = getMapSafe(graph.adjacency, node) if (Option.isSome(adjacencyList)) { for (const edgeIndex of adjacencyList.value) { const edge = getMapSafe(graph.edges, edgeIndex) if (Option.isSome(edge)) { queue.push(edge.value.target) } } } } // If target is affected by negative cycle, return null if (affectedNodes.has(target)) { return Option.none() } } } // Check if target is reachable const targetDistance = distances.get(target)! if (targetDistance === Infinity) { return Option.none() // No path exists } // Reconstruct path const path: Array = [] const edgeWeights: Array = [] let currentNode: NodeIndex | null = target while (currentNode !== null) { path.unshift(currentNode) const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)! if (prev !== null) { edgeWeights.unshift(prev.edgeData) currentNode = prev.node } else { currentNode = null } } return Option.some({ path, distance: targetDistance, edgeWeights }) } /** * Concrete class for iterables that produce [NodeIndex, NodeData] tuples. * * This class provides a common abstraction for all iterables that return node data, * including traversal iterators (DFS, BFS, etc.) and element iterators (nodes, externals). * It uses a mapEntry function pattern for flexible iteration and transformation. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, 1) * }) * * // Both traversal and element iterators return NodeWalker * const dfsNodes: Graph.NodeWalker = Graph.dfs(graph, { startNodes: [0] }) * const allNodes: Graph.NodeWalker = Graph.nodes(graph) * * // Common interface for working with node iterables * function processNodes(nodeIterable: Graph.NodeWalker): Array { * return Array.from(Graph.indices(nodeIterable)) * } * * // Access node data using values() or entries() * const nodeData = Array.from(Graph.values(dfsNodes)) // ["A", "B"] * const nodeEntries = Array.from(Graph.entries(allNodes)) // [[0, "A"], [1, "B"]] * ``` * * @since 3.18.0 * @category models */ export class Walker implements Iterable<[T, N]> { // @ts-ignore readonly [Symbol.iterator]: () => Iterator<[T, N]> /** * Visits each element and maps it to a value using the provided function. * * Takes a function that receives the index and data, * and returns an iterable of the mapped values. Skips elements that * no longer exist in the graph. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, 1) * }) * * const dfs = Graph.dfs(graph, { startNodes: [0] }) * * // Map to just the node data * const values = Array.from(dfs.visit((index, data) => data)) * console.log(values) // ["A", "B"] * * // Map to custom objects * const custom = Array.from(dfs.visit((index, data) => ({ id: index, name: data }))) * console.log(custom) // [{ id: 0, name: "A" }, { id: 1, name: "B" }] * ``` * * @since 3.18.0 * @category iterators */ readonly visit: (f: (index: T, data: N) => U) => Iterable constructor( /** * Visits each element and maps it to a value using the provided function. * * Takes a function that receives the index and data, * and returns an iterable of the mapped values. Skips elements that * no longer exist in the graph. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, 1) * }) * * const dfs = Graph.dfs(graph, { startNodes: [0] }) * * // Map to just the node data * const values = Array.from(dfs.visit((index, data) => data)) * console.log(values) // ["A", "B"] * * // Map to custom objects * const custom = Array.from(dfs.visit((index, data) => ({ id: index, name: data }))) * console.log(custom) // [{ id: 0, name: "A" }, { id: 1, name: "B" }] * ``` * * @since 3.18.0 * @category iterators */ visit: (f: (index: T, data: N) => U) => Iterable ) { this.visit = visit this[Symbol.iterator] = visit((index, data) => [index, data] as [T, N])[Symbol.iterator] } } /** * Type alias for node iteration using Walker. * NodeWalker is represented as Walker. * * @since 3.18.0 * @category models */ export type NodeWalker = Walker /** * Type alias for edge iteration using Walker. * EdgeWalker is represented as Walker>. * * @since 3.18.0 * @category models */ export type EdgeWalker = Walker> /** * Returns an iterator over the indices in the walker. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, 1) * }) * * const dfs = Graph.dfs(graph, { startNodes: [0] }) * const indices = Array.from(Graph.indices(dfs)) * console.log(indices) // [0, 1] * ``` * * @since 3.18.0 * @category utilities */ export const indices = (walker: Walker): Iterable => walker.visit((index, _) => index) /** * Returns an iterator over the values (data) in the walker. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, 1) * }) * * const dfs = Graph.dfs(graph, { startNodes: [0] }) * const values = Array.from(Graph.values(dfs)) * console.log(values) // ["A", "B"] * ``` * * @since 3.18.0 * @category utilities */ export const values = (walker: Walker): Iterable => walker.visit((_, data) => data) /** * Returns an iterator over [index, data] entries in the walker. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, 1) * }) * * const dfs = Graph.dfs(graph, { startNodes: [0] }) * const entries = Array.from(Graph.entries(dfs)) * console.log(entries) // [[0, "A"], [1, "B"]] * ``` * * @since 3.18.0 * @category utilities */ export const entries = (walker: Walker): Iterable<[T, N]> => walker.visit((index, data) => [index, data] as [T, N]) /** * Configuration options for DFS iterator. * * @since 3.18.0 * @category models */ export interface DfsConfig { readonly startNodes?: Array readonly direction?: Direction } /** * Creates a new DFS iterator with optional configuration. * * The iterator maintains a stack of nodes to visit and tracks discovered nodes. * It provides lazy evaluation of the depth-first search. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 1) * Graph.addEdge(mutable, b, c, 1) * }) * * // Start from a specific node * const dfs1 = Graph.dfs(graph, { startNodes: [0] }) * for (const nodeIndex of Graph.indices(dfs1)) { * console.log(nodeIndex) // Traverses in DFS order: 0, 1, 2 * } * * // Empty iterator (no starting nodes) * const dfs2 = Graph.dfs(graph) * // Can be used programmatically * ``` * * @since 3.18.0 * @category iterators */ export const dfs = ( graph: Graph | MutableGraph, config: DfsConfig = {} ): NodeWalker => { const startNodes = config.startNodes ?? [] const direction = config.direction ?? "outgoing" // Validate that all start nodes exist for (const nodeIndex of startNodes) { if (!hasNode(graph, nodeIndex)) { throw new Error(`Start node ${nodeIndex} does not exist`) } } return new Walker((f) => ({ [Symbol.iterator]: () => { const stack = [...startNodes] const discovered = new Set() const nextMapped = () => { while (stack.length > 0) { const current = stack.pop()! if (discovered.has(current)) { continue } discovered.add(current) const nodeDataOption = getMapSafe(graph.nodes, current) if (Option.isNone(nodeDataOption)) { continue } const neighbors = neighborsDirected(graph, current, direction) for (let i = neighbors.length - 1; i >= 0; i--) { const neighbor = neighbors[i] if (!discovered.has(neighbor)) { stack.push(neighbor) } } return { done: false, value: f(current, nodeDataOption.value) } } return { done: true, value: undefined } as const } return { next: nextMapped } } })) } /** * Configuration options for BFS iterator. * * @since 3.18.0 * @category models */ export interface BfsConfig { readonly startNodes?: Array readonly direction?: Direction } /** * Creates a new BFS iterator with optional configuration. * * The iterator maintains a queue of nodes to visit and tracks discovered nodes. * It provides lazy evaluation of the breadth-first search. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 1) * Graph.addEdge(mutable, b, c, 1) * }) * * // Start from a specific node * const bfs1 = Graph.bfs(graph, { startNodes: [0] }) * for (const nodeIndex of Graph.indices(bfs1)) { * console.log(nodeIndex) // Traverses in BFS order: 0, 1, 2 * } * * // Empty iterator (no starting nodes) * const bfs2 = Graph.bfs(graph) * // Can be used programmatically * ``` * * @since 3.18.0 * @category iterators */ export const bfs = ( graph: Graph | MutableGraph, config: BfsConfig = {} ): NodeWalker => { const startNodes = config.startNodes ?? [] const direction = config.direction ?? "outgoing" // Validate that all start nodes exist for (const nodeIndex of startNodes) { if (!hasNode(graph, nodeIndex)) { throw new Error(`Start node ${nodeIndex} does not exist`) } } return new Walker((f) => ({ [Symbol.iterator]: () => { const queue = [...startNodes] const discovered = new Set() const nextMapped = () => { while (queue.length > 0) { const current = queue.shift()! if (!discovered.has(current)) { discovered.add(current) const neighbors = neighborsDirected(graph, current, direction) for (const neighbor of neighbors) { if (!discovered.has(neighbor)) { queue.push(neighbor) } } const nodeData = getNode(graph, current) if (Option.isSome(nodeData)) { return { done: false, value: f(current, nodeData.value) } } return nextMapped() } } return { done: true, value: undefined } as const } return { next: nextMapped } } })) } /** * Configuration options for topological sort iterator. * * @since 3.18.0 * @category models */ export interface TopoConfig { readonly initials?: Array } /** * Creates a new topological sort iterator with optional configuration. * * The iterator uses Kahn's algorithm to lazily produce nodes in topological order. * Throws an error if the graph contains cycles. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 1) * Graph.addEdge(mutable, b, c, 1) * }) * * // Standard topological sort * const topo1 = Graph.topo(graph) * for (const nodeIndex of Graph.indices(topo1)) { * console.log(nodeIndex) // 0, 1, 2 (topological order) * } * * // With initial nodes * const topo2 = Graph.topo(graph, { initials: [0] }) * * // Throws error for cyclic graph * const cyclicGraph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * Graph.addEdge(mutable, a, b, 1) * Graph.addEdge(mutable, b, a, 2) // Creates cycle * }) * * try { * Graph.topo(cyclicGraph) // Throws: "Cannot perform topological sort on cyclic graph" * } catch (error) { * console.log((error as Error).message) * } * ``` * * @since 3.18.0 * @category iterators */ export const topo = ( graph: Graph | MutableGraph, config: TopoConfig = {} ): NodeWalker => { // Check if graph is acyclic first if (!isAcyclic(graph)) { throw new Error("Cannot perform topological sort on cyclic graph") } const initials = config.initials ?? [] // Validate that all initial nodes exist for (const nodeIndex of initials) { if (!hasNode(graph, nodeIndex)) { throw new Error(`Initial node ${nodeIndex} does not exist`) } } return new Walker((f) => ({ [Symbol.iterator]: () => { const inDegree = new Map() const remaining = new Set() const queue = [...initials] // Initialize in-degree counts for (const [nodeIndex] of graph.nodes) { inDegree.set(nodeIndex, 0) remaining.add(nodeIndex) } // Calculate in-degrees for (const [, edgeData] of graph.edges) { const currentInDegree = inDegree.get(edgeData.target) || 0 inDegree.set(edgeData.target, currentInDegree + 1) } // Add nodes with zero in-degree to queue if no initials provided if (initials.length === 0) { for (const [nodeIndex, degree] of inDegree) { if (degree === 0) { queue.push(nodeIndex) } } } const nextMapped = () => { while (queue.length > 0) { const current = queue.shift()! if (remaining.has(current)) { remaining.delete(current) // Process outgoing edges, reducing in-degree of targets const neighbors = neighborsDirected(graph, current, "outgoing") for (const neighbor of neighbors) { if (remaining.has(neighbor)) { const currentInDegree = inDegree.get(neighbor) || 0 const newInDegree = currentInDegree - 1 inDegree.set(neighbor, newInDegree) // If in-degree becomes 0, add to queue if (newInDegree === 0) { queue.push(neighbor) } } } const nodeData = getNode(graph, current) if (Option.isSome(nodeData)) { return { done: false, value: f(current, nodeData.value) } } return nextMapped() } } return { done: true, value: undefined } as const } return { next: nextMapped } } })) } /** * Configuration options for DFS postorder iterator. * * @since 3.18.0 * @category models */ export interface DfsPostOrderConfig { readonly startNodes?: Array readonly direction?: Direction } /** * Creates a new DFS postorder iterator with optional configuration. * * The iterator maintains a stack with visit state tracking and emits nodes * in postorder (after all descendants have been processed). Essential for * dependency resolution and tree destruction algorithms. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const root = Graph.addNode(mutable, "root") * const child1 = Graph.addNode(mutable, "child1") * const child2 = Graph.addNode(mutable, "child2") * Graph.addEdge(mutable, root, child1, 1) * Graph.addEdge(mutable, root, child2, 1) * }) * * // Postorder: children before parents * const postOrder = Graph.dfsPostOrder(graph, { startNodes: [0] }) * for (const node of postOrder) { * console.log(node) // 1, 2, 0 * } * ``` * * @since 3.18.0 * @category iterators */ export const dfsPostOrder = ( graph: Graph | MutableGraph, config: DfsPostOrderConfig = {} ): NodeWalker => { const startNodes = config.startNodes ?? [] const direction = config.direction ?? "outgoing" // Validate that all start nodes exist for (const nodeIndex of startNodes) { if (!hasNode(graph, nodeIndex)) { throw new Error(`Start node ${nodeIndex} does not exist`) } } return new Walker((f) => ({ [Symbol.iterator]: () => { const stack: Array<{ node: NodeIndex; visitedChildren: boolean }> = [] const discovered = new Set() const finished = new Set() // Initialize stack with start nodes for (let i = startNodes.length - 1; i >= 0; i--) { stack.push({ node: startNodes[i], visitedChildren: false }) } const nextMapped = () => { while (stack.length > 0) { const current = stack[stack.length - 1] if (!discovered.has(current.node)) { discovered.add(current.node) current.visitedChildren = false } if (!current.visitedChildren) { current.visitedChildren = true const neighbors = neighborsDirected(graph, current.node, direction) for (let i = neighbors.length - 1; i >= 0; i--) { const neighbor = neighbors[i] if (!discovered.has(neighbor) && !finished.has(neighbor)) { stack.push({ node: neighbor, visitedChildren: false }) } } } else { const nodeToEmit = stack.pop()!.node if (!finished.has(nodeToEmit)) { finished.add(nodeToEmit) const nodeData = getNode(graph, nodeToEmit) if (Option.isSome(nodeData)) { return { done: false, value: f(nodeToEmit, nodeData.value) } } return nextMapped() } } } return { done: true, value: undefined } as const } return { next: nextMapped } } })) } /** * Creates an iterator over all node indices in the graph. * * The iterator produces node indices in the order they were added to the graph. * This provides access to all nodes regardless of connectivity. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 1) * }) * * const indices = Array.from(Graph.indices(Graph.nodes(graph))) * console.log(indices) // [0, 1, 2] * ``` * * @since 3.18.0 * @category iterators */ export const nodes = ( graph: Graph | MutableGraph ): NodeWalker => new Walker((f) => ({ [Symbol.iterator]() { const nodeMap = graph.nodes const iterator = nodeMap.entries() return { next() { const result = iterator.next() if (result.done) { return { done: true, value: undefined } } const [nodeIndex, nodeData] = result.value return { done: false, value: f(nodeIndex, nodeData) } } } } })) /** * Creates an iterator over all edge indices in the graph. * * The iterator produces edge indices in the order they were added to the graph. * This provides access to all edges regardless of connectivity. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") * const b = Graph.addNode(mutable, "B") * const c = Graph.addNode(mutable, "C") * Graph.addEdge(mutable, a, b, 1) * Graph.addEdge(mutable, b, c, 2) * }) * * const indices = Array.from(Graph.indices(Graph.edges(graph))) * console.log(indices) // [0, 1] * ``` * * @since 3.18.0 * @category iterators */ export const edges = ( graph: Graph | MutableGraph ): EdgeWalker => new Walker((f) => ({ [Symbol.iterator]() { const edgeMap = graph.edges const iterator = edgeMap.entries() return { next() { const result = iterator.next() if (result.done) { return { done: true, value: undefined } } const [edgeIndex, edgeData] = result.value return { done: false, value: f(edgeIndex, edgeData) } } } } })) /** * Configuration for externals iterator. * * @since 3.18.0 * @category models */ export interface ExternalsConfig { readonly direction?: Direction } /** * Creates an iterator over external nodes (nodes without edges in specified direction). * * External nodes are nodes that have no outgoing edges (direction="outgoing") or * no incoming edges (direction="incoming"). These are useful for finding * sources, sinks, or isolated nodes. * * @example * ```ts * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { * const source = Graph.addNode(mutable, "source") // 0 - no incoming * const middle = Graph.addNode(mutable, "middle") // 1 - has both * const sink = Graph.addNode(mutable, "sink") // 2 - no outgoing * const isolated = Graph.addNode(mutable, "isolated") // 3 - no edges * * Graph.addEdge(mutable, source, middle, 1) * Graph.addEdge(mutable, middle, sink, 2) * }) * * // Nodes with no outgoing edges (sinks + isolated) * const sinks = Array.from(Graph.indices(Graph.externals(graph, { direction: "outgoing" }))) * console.log(sinks) // [2, 3] * * // Nodes with no incoming edges (sources + isolated) * const sources = Array.from(Graph.indices(Graph.externals(graph, { direction: "incoming" }))) * console.log(sources) // [0, 3] * ``` * * @since 3.18.0 * @category iterators */ export const externals = ( graph: Graph | MutableGraph, config: ExternalsConfig = {} ): NodeWalker => { const direction = config.direction ?? "outgoing" return new Walker((f) => ({ [Symbol.iterator]: () => { const nodeMap = graph.nodes const adjacencyMap = direction === "incoming" ? graph.reverseAdjacency : graph.adjacency const nodeIterator = nodeMap.entries() const nextMapped = () => { let current = nodeIterator.next() while (!current.done) { const [nodeIndex, nodeData] = current.value const adjacencyList = getMapSafe(adjacencyMap, nodeIndex) // Node is external if it has no edges in the specified direction if (Option.isNone(adjacencyList) || adjacencyList.value.length === 0) { return { done: false, value: f(nodeIndex, nodeData) } } current = nodeIterator.next() } return { done: true, value: undefined } as const } return { next: nextMapped } } })) }