Skip to content

Commit

Permalink
Update README
Browse files Browse the repository at this point in the history
  • Loading branch information
flowtoolz committed Jan 10, 2023
1 parent f2d715e commit d1e56db
Showing 1 changed file with 50 additions and 34 deletions.
84 changes: 50 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,94 +8,93 @@

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

## How to Edit, Query and Copy Graphs
### Design Goals

* Usability, safety, extensibility and maintainability – which also imply simplicity.
* 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?
* 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.

## How to Create, Edit and Query Graphs

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.

### Insert Values

A `Graph<NodeID: Hashable, NodeValue>` holds values of type `NodeValue` in nodes. Nodes are unique and have IDs of type `NodeID`:
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`:

```swift
let graph = Graph<String, Int> { "id\($0)" } // NodeID == String, NodeValue == Int
var graph = Graph<String, Int> { "id\($0)" } // NodeID == String, NodeValue == Int
let node = graph.insert(1) // node.id == "id1", node.value == 1

let nodeForID1 = graph.node(for: "id1") // nodeForID1 === node
let nodeForID1 = graph.node(for: "id1") // nodeForID1.id == "id1"
let valueForID1 = graph.value(for: "id1") // valueForID1 == 1
```

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

> 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.
### Generate Node IDs

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

```swift
let graph = Graph<UUID, Int> { _ in UUID() } // NodeID == UUID, NodeValue == Int
var graph = Graph<UUID, Int> { _ in UUID() } // NodeID == UUID, NodeValue == Int
let node1 = graph.insert(42)
let node2 = graph.insert(42) // node1 !== node2, same value in different nodes
let node2 = graph.insert(42) // node1.id != node2.id, same value in different nodes
```

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:

```swift
let graph = Graph<Int, Int>() // NodeID == NodeValue == Int
var graph = Graph<Int, Int>() // NodeID == NodeValue == Int
let node1 = graph.insert(42) // node1.value == node1.id == 42
let node2 = graph.insert(42) // node1 === node2 because 42 implies the same ID
let node2 = graph.insert(42) // node1.id == node2.id because 42 implies the same ID
```

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:

```swift
struct IdentifiableValue: Identifiable { let id = UUID() }
let graph = Graph<UUID, IdentifiableValue>() // NodeID == NodeValue.ID == UUID
var graph = Graph<UUID, IdentifiableValue>() // NodeID == NodeValue.ID == UUID
let node = graph.insert(IdentifiableValue()) // node.id == node.value.id
```

### Connect Nodes via Edges

```swift
let graph = Graph<String, Int> { "id\($0)" }
var graph = Graph<String, Int> { "id\($0)" }
let node1 = graph.insert(1)
let node2 = graph.insert(2)

// two ways to add an edge:
let edge = graph.addEdge(from: node1, to: node2) // by nodes
_ = graph.addEdge(from: node1.id, to: node2.id) // by node IDs

// same result: edge.origin === node1, edge.destination === node2
let edge = graph.addEdge(from: node1.id, to: node2.id)
```

An `edge` is directed and goes from its `edge.origin` node to its `edge.destination` node.
An `edge` is directed and goes from its `edge.originID` node ID to its `edge.destinationID` node ID.

### Specify Edge Counts

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:

```swift
graph.addEdge(from: node1, to: node2, count: 40) // edge count is 40
graph.addEdge(from: node1, to: node2, count: 2) // edge count is 42
graph.addEdge(from: node1.id, to: node2.id, count: 40) // edge count is 40
graph.addEdge(from: node1.id, to: node2.id, count: 2) // edge count is 42
```

### Remove Edges

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

```swift
let edgeID_A = Edge.ID(node1, node2)
let edgeID_B = Edge.ID(node1.id, node2.id) // edgeID_A == edgeID_B
let edgeID = Edge.ID(node1.id, node2.id)
```

This leads to six ways of removing an edge:
This leads to 3 ways of removing an edge:

```swift
let edge = graph.addEdge(from: node1, to: node2)
let edge = graph.addEdge(from: node1.id, to: node2.id)

graph.remove(edge)
graph.removeEdge(with: edge.id)
graph.removeEdge(with: .init(node1, node2))
graph.removeEdge(with: .init(node1.id, node2.id))
graph.removeEdge(from: node1, to: node2)
graph.removeEdge(from: node1.id, to: node2.id)
```

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

```swift
let graph = Graph<Int, Int>() // NodeID == NodeValue == Int
var graph = Graph<Int, Int>() // NodeID == NodeValue == Int
graph.insert(5)
graph.insert(3) // graph.values == [5, 3]
graph.sort { $0.id < $1.id } // graph.values == [3, 5]
```

### Copy a Graph

Many algorithms produce a variant of a given graph. Rather than modifying the original graph, SwiftNodes suggests to copy it.

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

```swift
let subsetCopy = graph.copy(includedNodes: [node2, node3],
includedEdges: [edge23])
var graph = Graph<Int, Int>()
/* then add a bunch of nodes and edges ... */
let subsetOfNodeIDs: Set<Int> = [0, 3, 6, 9, 12]
let subGraph = graph.subGraph(nodeIDs: subsetOfNodeIDs)
```

## Concurrency Safety

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

* You decide on mutability by using `var` or `let`.
* You can easily copy a whole `Graph`.
* You can use a `Graph` as a `@State` or `@Published` variable with SwiftUI.
* You can use property observers like `didSet` to observe changes in a `Graph`.

## How Algorithms Mark Nodes

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.

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

## Included Algorithms

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

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

## Future Directions

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.

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.

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.

## Architecture

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

0 comments on commit d1e56db

Please sign in to comment.