{"id":20519475,"url":"https://github.com/codeface-io/SwiftNodes","last_synced_at":"2025-05-09T09:31:23.661Z","repository":{"id":61189588,"uuid":"157186057","full_name":"codeface-io/SwiftNodes","owner":"codeface-io","description":"Concurrency Safe Graph in Swift + Graph Algorithms","archived":false,"fork":false,"pushed_at":"2023-06-26T15:28:24.000Z","size":7054,"stargazers_count":28,"open_issues_count":0,"forks_count":5,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-23T11:40:18.305Z","etag":null,"topics":["algorithms","data-structures","directed-acyclic-graph","directed-graph","graph","graph-algorithms","graph-drawing","swift"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/codeface-io.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2018-11-12T09:12:19.000Z","updated_at":"2024-09-16T18:39:22.000Z","dependencies_parsed_at":"2023-02-17T12:46:15.334Z","dependency_job_id":"b4ccfacd-dcea-42b1-9722-7780c41bc1e5","html_url":"https://github.com/codeface-io/SwiftNodes","commit_stats":{"total_commits":156,"total_committers":2,"mean_commits":78.0,"dds":"0.0064102564102563875","last_synced_commit":"7a96375ee2693a884f052bf36305b24dce5f8bcb"},"previous_names":["flowtoolz/swiftnodes"],"tags_count":19,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codeface-io%2FSwiftNodes","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codeface-io%2FSwiftNodes/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codeface-io%2FSwiftNodes/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codeface-io%2FSwiftNodes/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/codeface-io","download_url":"https://codeload.github.com/codeface-io/SwiftNodes/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253226468,"owners_count":21874333,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["algorithms","data-structures","directed-acyclic-graph","directed-graph","graph","graph-algorithms","graph-drawing","swift"],"created_at":"2024-11-15T22:14:10.560Z","updated_at":"2025-05-09T09:31:23.248Z","avatar_url":"https://github.com/codeface-io.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SwiftNodes\n\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fcodeface-io%2FSwiftNodes%2Fbadge%3Ftype%3Dswift-versions\u0026style=flat-square)](https://swiftpackageindex.com/codeface-io/SwiftNodes) \u0026nbsp;[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fcodeface-io%2FSwiftNodes%2Fbadge%3Ftype%3Dplatforms\u0026style=flat-square)](https://swiftpackageindex.com/codeface-io/SwiftNodes) \u0026nbsp;[![](https://img.shields.io/badge/Documentation-DocC-blue.svg?style=flat-square)](https://swiftpackageindex.com/codeface-io/SwiftNodes/documentation) \u0026nbsp;[![](https://img.shields.io/badge/License-MIT-lightgrey.svg?style=flat-square)](LICENSE)\n\n👩🏻‍🚀 *This project [is still a tad experimental](#development-status). Contributors and pioneers welcome!*\n\n## What?\n\nSwiftNodes offers a concurrency safe [graph data structure](https://en.wikipedia.org/wiki/Graph_(abstract_data_type)) together with graph algorithms. A graph stores values in identifiable nodes which can be connected via edges.\n\n### Contents\n\n* [Why?](#Why)\n* [How?](#How)\n* [Included Algorithms](#Included-Algorithms)\n* [Architecture](#Architecture)\n* [Development Status](#Development-Status)\n* [Roadmap](#Roadmap)\n\n## Why?\n\nGraphs may be the most fundamental mathematical concept besides numbers. They have wide applications in problem solving, data analysis and visualization. And although such data structures fit well with the language, graph implementations in Swift are lacking – in particular, comprehensive graph algorithm libraries.\n\nSwiftNodes and its included algorithms were extracted from [Codeface](https://codeface.io). But SwiftNodes is general enough to serve other applications as well – and extensible enough for more algorithms to be added.\n\n### Design Goals\n\n* Usability, safety, extensibility and maintainability – which also imply simplicity.\n* In particular, the API is supposed to feel familiar and fit well with official Swift data structures. So one question that guides its design is: What would Apple do?\n\nWe put the above qualities over performance. But that doesn't mean we neccessarily end up with suboptimal performance. The main compromise SwiftNodes involves is that nodes are value types and can not be referenced, so they must be hashed. But that doesn't change the average case complexity and, in the future, we might even be able to avoid that hashing in essential use cases by exploiting array indices.\n\n## How?\n\nThis section is a tutorial and touches only parts of the SwiftNodes API. We recommend exploring the [DocC reference](https://swiftpackageindex.com/codeface-io/SwiftNodes/documentation), [unit tests](Tests) and [production code](Code). The code in particular is actually small, meaninfully organized and easy to grasp.\n\n### Understand and Initialize Graphs\n\nLet's look at our first graph:\n\n```swift\nlet graph = Graph\u003cInt, Int, Double\u003e(values: [1, 2, 3],  // values serve as node IDs\n                                    edges: [(1, 2), (2, 3), (1, 3)])\n```\n\n`Graph` is generic over three types: `Graph\u003cNodeID: Hashable, NodeValue, EdgeWeight: Numeric\u003e`. Much like a `Dictionary` stores values for unique keys, a `Graph` stores values for unique node IDs. Actually, the `Graph` stores the values *within* its nodes which we identify by their IDs. Unlike a `Dictionary`, a `Graph` also allows to connect its unique \"value locations\", which are its node IDs. Those connections are the graph's edges, and each of them has a numeric weight.\n\nSo, in the above example, `Graph\u003cInt, Int, Double\u003e` stores `Int` values for `Int` node IDs and connects these node IDs (nodes) through edges that each have a `Double` weight. We provided the values and specified the edges. But where do the actual `Int` node IDs and `Double` edge weights come from? In both regards, the above initializer is a rather convenient one that infers things:\n\n1. When values and node IDs are of the same (and thereby hashable) type, SwiftNodes infers that we actually don't need distinct node IDs, so each unique value also serves as the ID of its own node.\n2. When we don't want to use or specify edge weights, we can specify edges by just the node IDs they connect, and SwiftNodes will create the corresponding edges with a default weight of 1.\n\nWe could explicitly provide distinct node IDs, for example of type `String`:\n\n```swift\nlet graph = Graph\u003cString, Int, Double\u003e(valuesByID: [\"a\": 1, \"b\": 2, \"c\": 3],\n                                       edges: [(\"a\", \"b\"), (\"b\", \"c\"), (\"a\", \"c\")])\n```\n\nAnd if we want to add all edges later, we can create graphs without edges via array- and dictionary literals:\n\n```swift\nlet graph = Graph\u003cInt, Int, Double\u003e = [1, 2, 3]  // values serve as node IDs\n        _ = Graph\u003cString, Int, Double\u003e = [\"a\": 1, \"b\": 2, \"c\": 3]\n```\n\nIn two of the above examples (1st and 3rd graph), SwiftNodes can infer node IDs because node values are of the same type. There is one other type of value with which we don't need to provide node IDs: node values that are `Identifiable` by the same type of ID as nodes are, i.e. `NodeID == NodeValue.ID`. In that case, each value's unique ID also serves as the ID of the value's node. This does not work with array literals but with initializers:\n\n```swift\nstruct IdentifiableValue: Identifiable { let id = UUID() }\ntypealias IVGraph = Graph\u003cUUID, IdentifiableValue, Double\u003e\n\nlet values = [IdentifiableValue(), IdentifiableValue(), IdentifiableValue()]\nlet ids = values.map { $0.id }\nlet graph = IVGraph(values: values,  // value IDs serve as node IDs \n                    edges: [(ids[0], ids[1]), (ids[1], ids[2]), (ids[0], ids[2])])\n```\n\nFor all initializer variants see [Graph.swift](Code/Graph/Graph.swift) and [Graph+ConvenientInitializers.swift](Code/Graph+CreateAndAccess/Graph+ConvenientInitializers.swift).\n\n### Values\n\nJust like with a `Dictionary`, you can read, write and delete values via subscripts and via functions:\n\n```swift\nvar graph = Graph\u003cString, Int, Double\u003e()\n\ngraph[\"a\"] = 1\nlet valueA = graph[\"a\"]\ngraph[\"a\"] = nil\n\ngraph.update(2, for: \"b\")  // returns the updated/created `Node` as `@discardableResult`\nlet valueB = graph.value(for: \"b\")\ngraph.removeValue(for: \"b\")  // returns the removed `NodeValue?` as `@discardableResult`\n        \nlet allValues = graph.values  // returns `some Collection`\n```\n\nAnd just like with the graph initializers, you don't need to provide node IDs if either the values themselves or their IDs can serve as node IDs. Here, values are identical to their node IDs:\n\n ```swift\n var graph = Graph\u003cInt, Int, Double\u003e()\n \n graph.insert(1)  // returns the updated/created `Node` as `@discardableResult`\n graph.remove(1)  // returns the removed `Node?` as `@discardableResult`\n ```\n\n### Edges\n\nEach edge is identified by the two nodes it connects, thus an edge ID is a combination of two node IDs. Edges are also directed, which means they point in a direction, from one node to another, which we might call \"origin-\" and \"destination node\" (or similar). Directed edges are the most general form. If a client or algorithm works with \"undirected\" graphs, that simply means it doesn't care about edge direction.\n\nThe three basic operations are inserting, reading and removing edges:\n\n```swift\nvar graph: Graph\u003cInt, Int, Double\u003e = [1, 2, 3]  // values serve as node IDs\n\ngraph.insertEdge(from: 1, to: 2)  // returns the edge as `@discardableResult`\nlet edge = graph.edge(from: 1, to: 2)  // the optional edge itself\nlet hasEdge = graph.containsEdge(from: 1, to: 2)  // whether the edge exists\ngraph.removeEdge(from: 1, to: 2)  // returns the optional edge as `@discardableResult`\n```\n\nOf course, `Graph` also has properties providing all edges, all edges by their IDs and all edge IDs. And it has ways to initialize and mutate edge weights. For the whole edge API, see [Graph.swift](Code/Graph/Graph.swift) and [Graph+EdgeAccess.swift](Code/Graph+CreateAndAccess/Graph+EdgeAccess.swift).\n\n### Nodes\n\nNodes are basically identifiable value containers that can be connected by edges. But aside from values they also store the IDs of neighbouring nodes. This redundant storage (cache) is kept up to date by the `Graph` and makes graph traversal a bit more performant and convenient. Any given `node` has these cache-based properties:\n\n```swift\nnode.descendantIDs  // IDs of all nodes to which there is an edge from node\nnode.ancestorIDs    // IDs of all nodes from which there is an edge to node\nnode.neighbourIDs   // all descendant- and ancestor IDs\nnode.isSink         // whether node has no descendants\nnode.isSource       // whether node has no ancestors\n```\n\nFor the whole node API, see [Graph.swift](Code/Graph/Graph.swift) and [Graph+NodeAccess.swift](Code/Graph+CreateAndAccess/Graph+NodeAccess.swift).\n\n### Marking Nodes\n\nMany graph algorithms do associate little intermediate results with individual nodes. The literature often refers to this as \"marking\" a node. The most prominent example is marking a node as visited while traversing a potentially cyclic graph. Some algorithms write multiple different markings to nodes. \n\nWhen we made SwiftNodes concurrency safe (to play well with the new Swift concurrency features), we removed the possibility to mark nodes directly, as that had lost its potential for performance optimization. See how the [included algorithms](Code/Graph+Algorithms) now use hashing to associate markings with nodes.\n\n### Value Semantics and Concurrency\n\nLike official Swift data structures, `Graph` is a pure `struct` and inherits the benefits of value types:\n\n* You decide on mutability by using `var` or `let`.\n* You can use a `Graph` as a `@State` or `@Published` variable with SwiftUI.\n* You can use property observers like `didSet` to observe changes in a `Graph`.\n* You can easily copy a whole `Graph`.\n\nMany algorithms produce a variant of a given graph. Rather than modifying the original graph, SwiftNodes suggests to copy it, and you can copy a `Graph` like any other value.\n\nA `Graph` is also `Sendable` **if** its value- and id type are. SwiftNodes is thereby ready for the strict concurrency safety of Swift 6. You can safely share `Sendable` `Graph` values between actors. Remember that, to declare a `Graph` property on a `Sendable` reference type, you need to make that property constant (use `let`).\n\n## Included Algorithms\n\nSwiftNodes has begun to accumulate [some graph algorithms](Code/Graph+Algorithms). The following overview also links to Wikipedia articles that explain what the algorithms do. We recommend also exploring them in code.\n\n### Filter and Map \n\nYou can map graph values and filter graphs by values, edges and nodes. Of course, the filters keep edges and node neighbour caches consistent and produce proper **subgraphs**.\n\n```swift\nlet graph: Graph\u003cInt, Int, Int\u003e = [1, 2, 10, 20]\n\nlet oneDigitGraph = graph.filtered { $0 \u003c 10 }\nlet stringGraph = graph.map { \"\\($0)\" }\n```\n\nSee all filters in [Graph+FilterAndMap.swift](Code/Graph+Algorithms/Graph+FilterAndMap.swift).\n\n### Components\n\n`graph.findComponents()`  returns multiple sets of node IDs which represent the [components](https://en.wikipedia.org/wiki/Component_(graph_theory)) of the `graph`.\n\n### Strongly Connected Components\n\n`graph.findStronglyConnectedComponents()`  returns multiple sets of node IDs which represent the [strongly connected components](https://en.wikipedia.org/wiki/Strongly_connected_component) of the `graph`.\n\n### Condensation Graph\n\n`graph.makeCondensationGraph()` creates the [condensation graph](https://en.wikipedia.org/wiki/Strongly_connected_component) of the `graph`, which is the graph in which all [strongly connected components](https://en.wikipedia.org/wiki/Strongly_connected_component) of the original `graph` have been collapsed into single nodes, so the resulting condensation graph is acyclic.\n\n### Transitive Reduction\n\n`graph.findTransitiveReductionEdges()` finds all edges of the [transitive reduction (the minimum equivalent graph)](https://en.wikipedia.org/wiki/Transitive_reduction) of the `graph`. You can also use `filterTransitiveReduction()` and `filteredTransitiveReduction()` to create a graph's [minimum equivalent graph](https://en.wikipedia.org/wiki/Transitive_reduction).\n\nRight now, all this only works on acyclic graphs and might even hang or crash on cyclic ones.\n\n### Essential Edges\n\n`graph.findEssentialEdges()` returns the IDs of all \"essential\" edges. You can also use `graph.filterEssentialEdges()` and `graph.filteredEssentialEdges()` to remove all \"non-essential\" edges from a `graph`.\n\nEdges are essential when they correspond to edges of the [MEG](https://en.wikipedia.org/wiki/Transitive_reduction) (the transitive reduction) of the [condensation graph](https://en.wikipedia.org/wiki/Strongly_connected_component). In simpler terms: Essential edges are either in cycles or they are essential to the reachability described by the graph – i.e. they cannot be removed without destroying the only path between some nodes.\n\nNote that only edges of the condensation graph can be non-essential and so edges in cycles (i.e. in strongly connected components) are all considered essential. This is because it's [algorithmically](https://en.wikipedia.org/wiki/Feedback_arc_set#Hardness) as well as conceptually hard to decide which edges in cycles are \"non-essential\". We recommend dealing with cycles independently of using this function.\n\n### Ancestor Counts\n\n`graph.findNumberOfNodeAncestors()` returns a `Dictionary\u003cNodeID, Int\u003e` containing the ancestor count for each node ID of the `graph`. The ancestor count is the number of all (recursive) ancestors of the node. Basically, it's the number of other nodes from which the node can be reached.\n\nThis only works on acyclic graphs right now and might return incorrect results for nodes in cycles.\n\nAncestor counts can serve as a proxy for [topological sorting](https://en.wikipedia.org/wiki/Topological_sorting).\n\n## Architecture\n\nHere is the architecture (composition and [essential](https://en.wikipedia.org/wiki/Transitive_reduction) dependencies) of the SwiftNodes code folder:\n\n![](Documentation/architecture.png)\n\nThe above image was created with [Codeface](https://codeface.io).\n\n## Development Status\n\nFrom version/tag 0.1.0 on, SwiftNodes adheres to [semantic versioning](https://semver.org). So until it has reached 1.0.0, its API may still break frequently, and we express those breaks with minor version bumps.\n\nSwiftNodes is already being used in production, but [Codeface](https://codeface.io) is still its primary client. SwiftNodes will move to version 1.0.0 as soon as **either one** of these conditions is met:\n\n* Basic practicality and conceptual soundness have been validated by serving multiple real-world clients.\n* We feel it's mature enough (well rounded and stable API, comprehensive tests, complete documentation and solid achievement of design goals).\n\n## Roadmap\n\n1. Review, update and complete all documentation, including API comments\n2. Review tests again and add more to cover the API comprehensively\n3. Round out and add algorithms (starting with the needs of Codeface):\n   1. Make existing algorithms compatible with cycles (two algorithms are still not). meaning: don't hang or crash, maybe throw an error!\n   2. Move to version 1.0.0 if possible\n   3. Add general purpose graph traversal algorithms (BFT, DFT, compatible with potentially cyclic graphs)\n   4. Add better ways of topological sorting\n   5. Approximate the [minimum feedback arc set](https://en.wikipedia.org/wiki/Feedback_arc_set), so Codeface can guess \"faulty\" or unintended dependencies, i.e. the fewest dependencies that need to be cut in order to break all cycles.\n4. Possibly optimize performance – but only based on measurements and only if measurements show that the optimization yields significant acceleration. Optimizing the algorithms might be more effective than optimizing the data structure itself.\n    * What role can `@inlinable` play here?\n    * What role can [`lazy`](https://developer.apple.com/documentation/swift/sequence/lazy) play here?\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodeface-io%2FSwiftNodes","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcodeface-io%2FSwiftNodes","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodeface-io%2FSwiftNodes/lists"}