Skip to content

Commit d1e56db

Browse files
committed
Update README
1 parent f2d715e commit d1e56db

File tree

1 file changed

+50
-34
lines changed

1 file changed

+50
-34
lines changed

README.md

+50-34
Original file line numberDiff line numberDiff line change
@@ -8,94 +8,93 @@
88

99
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).
1010

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
1218

1319
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.
1420

1521
### Insert Values
1622

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`:
1824

1925
```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
2127
let node = graph.insert(1) // node.id == "id1", node.value == 1
2228

23-
let nodeForID1 = graph.node(for: "id1") // nodeForID1 === node
29+
let nodeForID1 = graph.node(for: "id1") // nodeForID1.id == "id1"
2430
let valueForID1 = graph.value(for: "id1") // valueForID1 == 1
2531
```
2632

2733
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`.
2834

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+
2937
### Generate Node IDs
3038

3139
You may generate `NodeID`s independent of `NodeValue`s:
3240

3341
```swift
34-
let graph = Graph<UUID, Int> { _ in UUID() } // NodeID == UUID, NodeValue == Int
42+
var graph = Graph<UUID, Int> { _ in UUID() } // NodeID == UUID, NodeValue == Int
3543
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
3745
```
3846

3947
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:
4048

4149
```swift
42-
let graph = Graph<Int, Int>() // NodeID == NodeValue == Int
50+
var graph = Graph<Int, Int>() // NodeID == NodeValue == Int
4351
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
4553
```
4654

4755
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:
4856

4957
```swift
5058
struct IdentifiableValue: Identifiable { let id = UUID() }
51-
let graph = Graph<UUID, IdentifiableValue>() // NodeID == NodeValue.ID == UUID
59+
var graph = Graph<UUID, IdentifiableValue>() // NodeID == NodeValue.ID == UUID
5260
let node = graph.insert(IdentifiableValue()) // node.id == node.value.id
5361
```
5462

5563
### Connect Nodes via Edges
5664

5765
```swift
58-
let graph = Graph<String, Int> { "id\($0)" }
66+
var graph = Graph<String, Int> { "id\($0)" }
5967
let node1 = graph.insert(1)
6068
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)
6770
```
6871

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.
7073

7174
### Specify Edge Counts
7275

7376
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:
7477

7578
```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
7881
```
7982

8083
### Remove Edges
8184

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:
8386

8487
```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)
8789
```
8890

89-
This leads to six ways of removing an edge:
91+
This leads to 3 ways of removing an edge:
9092

9193
```swift
92-
let edge = graph.addEdge(from: node1, to: node2)
94+
let edge = graph.addEdge(from: node1.id, to: node2.id)
9395

94-
graph.remove(edge)
9596
graph.removeEdge(with: edge.id)
96-
graph.removeEdge(with: .init(node1, node2))
9797
graph.removeEdge(with: .init(node1.id, node2.id))
98-
graph.removeEdge(from: node1, to: node2)
9998
graph.removeEdge(from: node1.id, to: node2.id)
10099
```
101100

@@ -116,28 +115,37 @@ node.isSource // whether node has no ancestors
116115
The nodes in a `Graph` maintain an order. So you can also sort them:
117116

118117
```swift
119-
let graph = Graph<Int, Int>() // NodeID == NodeValue == Int
118+
var graph = Graph<Int, Int>() // NodeID == NodeValue == Int
120119
graph.insert(5)
121120
graph.insert(3) // graph.values == [5, 3]
122121
graph.sort { $0.id < $1.id } // graph.values == [3, 5]
123122
```
124123

125124
### Copy a Graph
126125

127-
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:...)`:
130127

131128
```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)
134133
```
135134

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+
136144
## How Algorithms Mark Nodes
137145

138146
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.
139147

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.
141149

142150
## Included Algorithms
143151

@@ -167,6 +175,14 @@ This only works on acyclic graphs right now and might return incorrect results f
167175

168176
Ancestor counts can serve as a proxy for [topological sorting](https://en.wikipedia.org/wiki/Topological_sorting).
169177

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+
170186
## Architecture
171187

172188
Here is the internal architecture (composition and [essential](https://en.wikipedia.org/wiki/Transitive_reduction) dependencies) of the SwiftNodes code folder:

0 commit comments

Comments
 (0)