GenGraph

GenGraph provides a way to build graph and tree data structures using GenServer-based nodes. Each node in a GenGraph is a GenServer process that can maintain edges to other nodes, enabling concurrent operations and automatic cleanup when processes terminate.

Features

Installation

If available in Hex, the package can be installed by adding gen_graph to your list of dependencies in mix.exs:

def deps do
  [
    {:gen_graph, "~> 0.1.1"}
  ]
end

Quick Start

Basic Graph Usage

# Define a node module
defmodule MyNode do
  use GenGraph.Node, [
    data: nil,
    name: ""
  ]
end

# Create nodes
node1 = MyNode.new(data: "first", name: "node1")
node2 = MyNode.new(data: "second", name: "node2")
node3 = MyNode.new(data: "third", name: "node3")

# Add edges between nodes
node1 = MyNode.add_edge(node1, node2)
node1 = MyNode.add_edge(node1, node3, weight: 5)

# Create bidirectional edges
node2 = MyNode.add_edge(node2, node3, bidirectional: true)

# Check edges
IO.inspect(node1.edges)  # [{node2.pid, 0}, {node3.pid, 5}]
IO.inspect(node2.edges)  # [{node3.pid, 0}]
IO.inspect(node3.edges)  # [{node2.pid, 0}]

Tree Usage

# Define a tree node module
defmodule TreeNode do
  use GenGraph.Tree, [
    name: "",
    data: nil
  ]
end

# Create tree structure
root = TreeNode.new(name: "root")
child1 = TreeNode.new(name: "child1")
child2 = TreeNode.new(name: "child2")
grandchild = TreeNode.new(name: "grandchild")

# Build tree relationships
root = TreeNode.add_child(root, child1)
root = TreeNode.add_child(root, child2)
child1 = TreeNode.add_child(child1, grandchild)

# Check tree structure
IO.inspect(root.child_nodes)     # [child1.pid, child2.pid]
IO.inspect(child1.child_nodes)   # [grandchild.pid]
IO.inspect(TreeNode.get(grandchild, :parent_pid)) # child1.pid

# Cycle prevention - this will return :error
TreeNode.add_child(grandchild, root)  # :error

Core Concepts

Nodes

Every node in GenGraph is a GenServer process created using GenObject. Nodes maintain:

Edges

Edges represent connections between nodes and support:

Process Monitoring

GenGraph uses Erlang's process monitoring to ensure data consistency:

API Reference

GenGraph.Node

add_edge(from, to, opts \\ [])

Adds an edge between two nodes with optional configuration.

Options:

Examples:

# Simple edge
node1 = MyNode.add_edge(node1, node2)

# Weighted edge
node1 = MyNode.add_edge(node1, node2, weight: 5)

# Bidirectional edge
node1 = MyNode.add_edge(node1, node2, bidirectional: true)

add_edge!(from, to, opts \\ [])

Asynchronously adds an edge using GenServer.cast for better performance.

remove_edge(from, to, opts \\ [])

Removes an edge between two nodes.

Options:

remove_edge!(from, to, opts \\ [])

Asynchronously removes an edge using GenServer.cast.

GenGraph.Tree

Tree nodes inherit all GenGraph.Node functionality plus:

add_child(parent, child, opts \\ [])

Adds a child node to a parent with cycle detection.

Returns: Updated parent struct or :error if operation would create a cycle.

add_child!(parent, child, opts \\ [])

Asynchronously adds a child (no cycle detection).

remove_child(parent, child, opts \\ [])

Removes a child node from a parent.

remove_child!(parent, child, opts \\ [])

Asynchronously removes a child node.

GenServer Callbacks

Both GenGraph.Node and GenGraph.Tree implement GenServer callbacks that handle the internal messaging for edge management:

GenGraph.Node Callbacks

GenGraph.Tree Callbacks

Tree nodes extend the Node callbacks with additional behavior:

Private Helper Functions

Advanced Features

Cycle Prevention

Tree nodes automatically prevent circular references:

parent = TreeNode.new()
child = TreeNode.new()
grandchild = TreeNode.new()

parent = TreeNode.add_child(parent, child)
child = TreeNode.add_child(child, grandchild)

# This will return :error due to cycle detection
TreeNode.add_child(grandchild, parent)  # :error

Automatic Cleanup

When a node process terminates, all edges pointing to it are automatically cleaned up:

node1 = MyNode.new()
node2 = MyNode.new()

node1 = MyNode.add_edge(node1, node2)
IO.inspect(length(node1.edges))  # 1

# Kill node2
Process.exit(node2.pid, :kill)
:timer.sleep(10)

node1 = MyNode.get(node1)
IO.inspect(length(node1.edges))  # 0 - edge automatically removed

Working with PIDs and Structs

All functions accept either PIDs or GenObject structs:

# These are equivalent
MyNode.add_edge(node1, node2)
MyNode.add_edge(node1.pid, node2.pid)
MyNode.add_edge(node1, node2.pid)
MyNode.add_edge(node1.pid, node2)

GenObject Integration

GenGraph is built on top of GenObject, which provides:

Each node automatically gets GenObject methods like:

Testing

Run the test suite:

mix test

The test suite includes comprehensive tests for:

License

This project is licensed under the MIT License - see the LICENSE.md file for details.

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

Acknowledgments

Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/gen_graph.