You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+50-34
Original file line number
Diff line number
Diff line change
@@ -8,94 +8,93 @@
8
8
9
9
SwiftNodes provides a [`Graph` data structure](https://en.wikipedia.org/wiki/Graph_(abstract_data_type)) together with graph algorithms. A `Graph` stores values in nodes which can be connected via edges. SwiftNodes was first used in production by [Codeface](https://codeface.io).
10
10
11
-
## How to Edit, Query and Copy Graphs
11
+
### Design Goals
12
+
13
+
* Usability, safety, extensibility and maintainability – which also imply simplicity.
14
+
* In particular, the API is supposed to be familiar and fit well with official Swift data structures. So one question that has started to guide its design is: What would Apple do?
15
+
* We put the above qualities over performance. But that doesn't even mean we neccessarily end up with suboptimal performance. The only compromise SwiftNodes currently involves is that nodes are value types and can not be referenced, so they must be hashed. We might be able to solve even that for essential use cases by exploting array indices and accepting lower sorting performance.
16
+
17
+
## How to Create, Edit and Query Graphs
12
18
13
19
The following explanations touch only parts of the SwiftNodes API. We recommend exploring the [DocC reference](https://swiftpackageindex.com/codeface-io/SwiftNodes/documentation), [unit tests](https://github.com/codeface-io/SwiftNodes/tree/master/Tests) and [production code](https://github.com/codeface-io/SwiftNodes/tree/master/Code). The code in particular is actually small and easy to grasp.
14
20
15
21
### Insert Values
16
22
17
-
A `Graph<NodeID: Hashable, NodeValue>` holds values of type `NodeValue` in nodes. Nodes are unique and have IDs of type `NodeID`:
23
+
A `Graph<NodeID: Hashable, NodeValue>` holds values of type `NodeValue` in nodes of type `GraphNode<NodeID: Hashable, NodeValue>`. Nodes are unique and have IDs of type `NodeID`:
18
24
19
25
```swift
20
-
let graph = Graph<String, Int> { "id\($0)" } // NodeID == String, NodeValue == Int
26
+
var graph = Graph<String, Int> { "id\($0)" } // NodeID == String, NodeValue == Int
let nodeForID1 = graph.node(for: "id1") // nodeForID1 === node
29
+
let nodeForID1 = graph.node(for: "id1") // nodeForID1.id == "id1"
24
30
let valueForID1 = graph.value(for: "id1") // valueForID1 == 1
25
31
```
26
32
27
33
When inserting a value, a `Graph` must know how to generate the ID of the node that would store the value. So the `Graph` initializer takes a closure returning a `NodeID` given a `NodeValue`.
28
34
35
+
> Side Note: The reason, there's an explicit node type at all is that a) values don't need to be unique, but nodes in a graph are, and b) a node holds caches for quick access to its neighbours. The reason there is an explicit edge type at all is that edges have a count (they are "weighted") and may hold their own values in the future.
36
+
29
37
### Generate Node IDs
30
38
31
39
You may generate `NodeID`s independent of `NodeValue`s:
32
40
33
41
```swift
34
-
let graph = Graph<UUID, Int> { _inUUID() } // NodeID == UUID, NodeValue == Int
42
+
var graph = Graph<UUID, Int> { _inUUID() } // NodeID == UUID, NodeValue == Int
35
43
let node1 = graph.insert(42)
36
-
let node2 = graph.insert(42) // node1 !== node2, same value in different nodes
44
+
let node2 = graph.insert(42) // node1.id != node2.id, same value in different nodes
37
45
```
38
46
39
47
If `NodeID` and `NodeValue` are the same type, you can omit the closure and the `Graph` will assume the value is itself used as the node ID:
40
48
41
49
```swift
42
-
let graph = Graph<Int, Int>() // NodeID == NodeValue == Int
50
+
var graph = Graph<Int, Int>() // NodeID == NodeValue == Int
43
51
let node1 = graph.insert(42) // node1.value == node1.id == 42
44
-
let node2 = graph.insert(42) // node1 === node2 because 42 implies the same ID
52
+
let node2 = graph.insert(42) // node1.id == node2.id because 42 implies the same ID
45
53
```
46
54
47
55
And if your `NodeValue` is itself `Identifiable` by IDs of type `NodeID`, then you can also omit the closure and `Graph` will use the `ID` of a `NodeValue` as the `NodeID` of the node holding that value:
48
56
49
57
```swift
50
58
structIdentifiableValue: Identifiable { let id =UUID() }
let node = graph.insert(IdentifiableValue()) // node.id == node.value.id
53
61
```
54
62
55
63
### Connect Nodes via Edges
56
64
57
65
```swift
58
-
let graph = Graph<String, Int> { "id\($0)" }
66
+
var graph = Graph<String, Int> { "id\($0)" }
59
67
let node1 = graph.insert(1)
60
68
let node2 = graph.insert(2)
61
-
62
-
// two ways to add an edge:
63
-
let edge = graph.addEdge(from: node1, to: node2) // by nodes
64
-
_= graph.addEdge(from: node1.id, to: node2.id) // by node IDs
65
-
66
-
// same result: edge.origin === node1, edge.destination === node2
69
+
let edge = graph.addEdge(from: node1.id, to: node2.id)
67
70
```
68
71
69
-
An `edge` is directed and goes from its `edge.origin` node to its `edge.destination` node.
72
+
An `edge` is directed and goes from its `edge.originID` node ID to its `edge.destinationID` node ID.
70
73
71
74
### Specify Edge Counts
72
75
73
76
Every `edge` has an integer count accessible via `edge.count`. It is more specifically a "count" rather than a "weight", as it increases when the same edge is added again. By default, a new edge has `count` 1 and adding it again increases its `count` by 1. But you can specify a custom count when adding an edge:
74
77
75
78
```swift
76
-
graph.addEdge(from: node1, to: node2, count: 40) // edge count is 40
77
-
graph.addEdge(from: node1, to: node2, count: 2) // edge count is 42
79
+
graph.addEdge(from: node1.id, to: node2.id, count: 40) // edge count is 40
80
+
graph.addEdge(from: node1.id, to: node2.id, count: 2) // edge count is 42
78
81
```
79
82
80
83
### Remove Edges
81
84
82
-
A `GraphEdge<NodeID: Hashable, NodeValue>` has its own `ID` type which combines the `NodeID`s of the edge's `origin`- and `destination` nodes. In the context of a `Graph` or `GraphEdge`, you can create edge IDs easily in two ways:
85
+
A `GraphEdge<NodeID: Hashable, NodeValue>` has its own `ID` type which combines the edge's `originID`- and `destinationID` node IDs. In the context of a `Graph` or `GraphEdge`, you can create edge IDs like so:
83
86
84
87
```swift
85
-
let edgeID_A = Edge.ID(node1, node2)
86
-
let edgeID_B = Edge.ID(node1.id, node2.id) // edgeID_A == edgeID_B
88
+
let edgeID = Edge.ID(node1.id, node2.id)
87
89
```
88
90
89
-
This leads to six ways of removing an edge:
91
+
This leads to 3 ways of removing an edge:
90
92
91
93
```swift
92
-
let edge = graph.addEdge(from: node1, to: node2)
94
+
let edge = graph.addEdge(from: node1.id, to: node2.id)
93
95
94
-
graph.remove(edge)
95
96
graph.removeEdge(with: edge.id)
96
-
graph.removeEdge(with: .init(node1, node2))
97
97
graph.removeEdge(with: .init(node1.id, node2.id))
98
-
graph.removeEdge(from: node1, to: node2)
99
98
graph.removeEdge(from: node1.id, to: node2.id)
100
99
```
101
100
@@ -116,28 +115,37 @@ node.isSource // whether node has no ancestors
116
115
The nodes in a `Graph` maintain an order. So you can also sort them:
117
116
118
117
```swift
119
-
let graph = Graph<Int, Int>() // NodeID == NodeValue == Int
118
+
var graph = Graph<Int, Int>() // NodeID == NodeValue == Int
Many algorithms produce a variant of a given graph. Rather than modifying the original graph, SwiftNodes suggests to copy it.
128
-
129
-
A `graph.copy()` is identical to the original `graph` in IDs, values and structure but contains its own new node- and edge objects. You may also copy just a subset of a `graph` and limit the included edges and/or nodes:
126
+
Many algorithms produce a variant of a given graph. Rather than modifying the original graph, SwiftNodes suggests to copy it. Since Graph is a `struct`, you copy it like any other value type. But right now, SwiftNodes only lets you add and remove edges – not nodes. To create a subgraph with a **subset** of the nodes of a `graph`, you can use `graph.subGraph(nodeIDs:...)`:
130
127
131
128
```swift
132
-
let subsetCopy = graph.copy(includedNodes: [node2, node3],
133
-
includedEdges: [edge23])
129
+
var graph = Graph<Int, Int>()
130
+
/* then add a bunch of nodes and edges ... */
131
+
let subsetOfNodeIDs: Set<Int> = [0, 3, 6, 9, 12]
132
+
let subGraph = graph.subGraph(nodeIDs: subsetOfNodeIDs)
134
133
```
135
134
135
+
## Concurrency Safety
136
+
137
+
`Graph` is `Sendable` and thereby ready for the strict concurrency safety of Swift 6. Like the official Swift data structures, `Graph` is even a pure `struct` and inherits the benefits of value types:
138
+
139
+
* You decide on mutability by using `var` or `let`.
140
+
* You can easily copy a whole `Graph`.
141
+
* You can use a `Graph` as a `@State` or `@Published` variable with SwiftUI.
142
+
* You can use property observers like `didSet` to observe changes in a `Graph`.
143
+
136
144
## How Algorithms Mark Nodes
137
145
138
146
Many 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.
139
147
140
-
In an effort to make SwiftNodes concurrency safe and play well with the new Swift concurrency features, we removed the possibility to mark nodes directly. See how the [included algorithms](https://github.com/codeface-io/SwiftNodes/tree/master/Code/Graph%2BAlgorithms) now associate markings with nodes via hashing.
148
+
In an effort to make SwiftNodes concurrency safe and play well with the new Swift concurrency features, we removed the possibility to mark nodes directly. See how the [included algorithms](https://github.com/codeface-io/SwiftNodes/tree/master/Code/Graph%2BAlgorithms) now use hashing to associate markings with nodes.
141
149
142
150
## Included Algorithms
143
151
@@ -167,6 +175,14 @@ This only works on acyclic graphs right now and might return incorrect results f
167
175
168
176
Ancestor counts can serve as a proxy for [topological sorting](https://en.wikipedia.org/wiki/Topological_sorting).
169
177
178
+
## Future Directions
179
+
180
+
For the included algorithms and current clients, the above described editing capabilities seem to suffice. Also, to make a `Graph` part of a `Sendable` type, you would need to hold it as a constant anyway. So, regarding editing, following development steps will focus on creating complete graphs with edges via initializers – rather than on editing `Graph` variables.
181
+
182
+
But an interesting future direction is certainly to further align `Graph` with the official Swift data structures and to provide an arsenal of synchronous and asynchronous filtering- and mapping functions.
183
+
184
+
Also, since `Graph` is (now) a full value type, public API and internal implementation should only use IDs instead of complete node- and edge values unless where necessary. The public `Graph` API is already free of requiring any edge- or node value arguments, but the algorithms have not been migrated in that way yet.
185
+
170
186
## Architecture
171
187
172
188
Here is the internal architecture (composition and [essential](https://en.wikipedia.org/wiki/Transitive_reduction) dependencies) of the SwiftNodes code folder:
0 commit comments