1 stable release

1.0.0 Jul 15, 2023

#2021 in Data structures

MPL-2.0 license

18KB
289 lines



Smart references to your graph nodes.

internode

crates.io docs.rs License



internode provides a hassle-free way to manage ownership of graph structures.

Features

  • Ownership propagation to connected nodes
  • Leak-free cyclic references
  • Customizable node implementation
  • Thread-safety
  • Do One Thing and Do It Well™︎
  • Almost zero dependency (only genawaiter until generators stabilize)

Usage

At first, define the shape of your nodes. In this example, we'll use a simple implementation that holds a pair of lists that represents in- and out-neighbors. The important point here is, inter-node references should be held through Internode:

use internode::*;

#[derive(Default)]
struct Entity {
	succs: Vec<Internode<Entity>>,
	preds: Vec<Internode<Entity>>,
}

impl Entity {
	fn add_edge(from: &Internode<Entity>, to: &Internode<Entity>) {
		// Accessing the content of node is done by `lock`.
		from.lock().unwrap().succs.push(to.clone());
		to.lock().unwrap().preds.push(from.clone());
	}
}

impl Neighbors for Entity {
	type Iter<'a> = std::iter::Cloned<std::slice::Iter<'a, Internode<Entity>>>;
	fn outgoing(&self) -> Self::Iter<'_> { self.succs.iter().cloned() }
	fn incoming(&self) -> Self::Iter<'_> { self.preds.iter().cloned() }
}

Then, create nodes by Node::new:

let (a_weak, b) = {
	// `Node` is owning reference, while `Internode` is non-owning.
	let a = Node::new(Entity::default());
	let b = Node::new(Entity::default());
	// `Node` implements `Deref` to `Internode`.
	Entity::add_edge(&*a, &*b);
	// Downgrading `Node` yields `Internode`.
	let a_weak = a.downgrade();
	(a_weak, b)
	// `a` is dropped here.
};
// Downgrading `Internode` yields `Option<Node>`.
assert!(a_weak.upgrade().is_some());
// Dropping the last owning reference to the graph drops all nodes.
drop(b);
assert!(a_weak.upgrade().is_none());

Cyclic Graphs

Cyclic references do work out of the box without memory leaks:

let (a_weak, b_weak, c_weak, c) = {
	let a = Node::new(Entity::default());
	let b = Node::new(Entity::default());
	let c = Node::new(Entity::default());
	Entity::add_edge(&*a, &*b);
	Entity::add_edge(&*b, &*c);
	Entity::add_edge(&*c, &*a);
	let a_weak = a.downgrade();
	let b_weak = b.downgrade();
	let c_weak = c.downgrade();
	(a_weak, b_weak, c_weak, c)
	// `a` and `b` are dropped here.
};
assert!(a_weak.upgrade().is_some());
assert!(b_weak.upgrade().is_some());
assert!(c_weak.upgrade().is_some());
drop(c);
assert!(a_weak.upgrade().is_none());
assert!(b_weak.upgrade().is_none());
assert!(c_weak.upgrade().is_none());

So you don't need to scratch your head about managing cycles anymore.

Node Traversal

As a bonus for implementing Neighbors, methods to perform depth- and breadth-first searching are provided:

let a = Node::new(Entity::default());
let b = Node::new(Entity::default());
let c = Node::new(Entity::default());
let d = Node::new(Entity::default());
Entity::add_edge(&*a, &*b);
Entity::add_edge(&*a, &*c);
Entity::add_edge(&*b, &*d);
Entity::add_edge(&*c, &*d);
Entity::add_edge(&*d, &*a);
assert!(a.dfs_outgoing().eq([&*a, &*b, &*d, &*c].into_iter().cloned()));
assert!(a.dfs_incoming().eq([&*a, &*d, &*b, &*c].into_iter().cloned()));
assert!(a.bfs_outgoing().eq([&*a, &*b, &*c, &*d].into_iter().cloned()));
assert!(a.bfs_incoming().eq([&*a, &*d, &*b, &*c].into_iter().cloned()));

Design Consideration

This crate is inspired by dendron (especially the concept that “reference to any node preserves entire tree”), which is limited to tree structures to ensure good properties, has a bunch of useful methods to manipulate, and also has defensive programming features like freezing nodes against edits. Such advanced functionalities are out of scope of internode and left to users, since requirements vary. For example, if you want your nodes to be frozen, then frozen or more stringent implementation is nice to have.

Dependencies

~100KB