Graph Operations Reference#
This reference covers the core graph operations in Jac for creating, connecting, traversing, and deleting nodes.
Related:
- Walker Responses - Understanding
.reportsand response handling- Part III: OSP - Walker and node fundamentals
- Build a Todo App - Full tutorial using these patterns
Node Creation and Connection#
Create and Connect (++>)#
The ++> operator creates a new node and connects it to the source node:
node Todo {
has id: str;
has title: str;
has completed: bool = False;
}
walker CreateTodo {
can create with `root entry {
# Create a Todo node and connect it to the current node (here)
new_node = here ++> Todo(
id="123",
title="Buy groceries",
completed=False
);
# The result is a list containing the new node
created_todo = new_node[0];
report created_todo;
}
}
Key points:
- Returns a list containing the created node(s)
- Access the node with
[0]index - The new node is automatically connected to
herewith an edge - Works with any node type
Connect Existing Nodes#
node MyNode {
has name: str;
}
edge EdgeType {}
with entry {
node_a = MyNode(name="A");
node_b = MyNode(name="B");
# Connect two existing nodes
node_a ++> node_b;
# Connect with edge type
node_a +>: EdgeType() :+> node_b;
}
Node Traversal#
Visit Outgoing Edges ([-->])#
Visit Incoming Edges ([<--])#
Visit Both Directions ([<-->])#
can traverse with SomeNode entry {
visit [<-->]; # Visit all connected nodes regardless of direction
}
Visit with Edge Type Filter#
edge MyEdgeType {
has weight: int = 0;
}
walker FilteredTraversal {
can traverse with `root entry {
# Only visit nodes connected by a specific edge type
visit [->:MyEdgeType:->];
}
can traverse_weighted with `root entry {
# Visit with edge condition
visit [->:MyEdgeType:weight > 10:->];
}
}
Context Keywords#
here - Current Node#
here refers to the node the walker is currently visiting:
can process with Todo entry {
# Read properties
print(here.title);
print(here.completed);
# Modify properties
here.completed = True;
here.title = "Updated title";
}
self - Walker Instance#
self refers to the walker itself:
walker:priv MyWalker {
has search_term: str;
has results: list = [];
can search with Item entry {
if self.search_term in here.name {
self.results.append(here);
}
}
}
root - User's Root Node#
root is the entry point for the user's graph:
node SomeNode {}
walker MyWalker {
can work with `root entry {
report "done";
}
}
walker GetRootData {
can get with `root entry {
report "root data";
}
}
walker ProcessNode {
# In a walker, access root explicitly
can process with SomeNode entry {
root_data = root spawn GetRootData();
}
}
with entry {
# Spawn a walker from root
result = root spawn MyWalker();
}
Node Deletion#
Delete Current Node (del here)#
can delete_if_done with Todo entry {
if here.completed {
del here; # Remove this node from the graph
report {"deleted": here.id};
}
}
Cascade Deletion Pattern#
Delete a node and all its related nodes:
walker:priv DeleteWithChildren {
has parent_id: str;
can search with `root entry {
visit [-->];
}
can delete with Todo entry {
# Delete if this is the target or a child of the target
if here.id == self.parent_id or here.parent_id == self.parent_id {
del here;
}
}
}
Walker Entry/Exit Points#
Entry Points#
walker:priv MyWalker {
# Runs when entering the root node
can on_root with `root entry {
visit [-->];
}
# Runs when entering any Todo node
can on_todo with Todo entry {
process(here);
}
# Runs when entering any node (generic)
can on_any with entry {
log("Visited a node");
}
}
Exit Points#
walker:priv MyWalker {
has collected: list = [];
can collect with Item entry {
self.collected.append(here.data);
}
# Runs when exiting root (after all traversal complete)
can finish with `root exit {
report self.collected;
}
# Runs when exiting any node
can leaving with exit {
log("Left a node");
}
}
Common Graph Patterns#
Pattern 1: CRUD Walker#
# Create
walker:priv CreateItem {
has name: str;
can create with `root entry {
new_item = here ++> Item(name=self.name);
report new_item[0];
}
}
# Read (List)
walker:priv ListItems {
has items: list = [];
can collect with `root entry { visit [-->]; }
can gather with Item entry { self.items.append(here); }
can finish with `root exit { report self.items; }
}
# Update
walker:priv UpdateItem {
has item_id: str;
has new_name: str;
can find with `root entry { visit [-->]; }
can update with Item entry {
if here.id == self.item_id {
here.name = self.new_name;
report here;
}
}
}
# Delete
walker:priv DeleteItem {
has item_id: str;
can find with `root entry { visit [-->]; }
can remove with Item entry {
if here.id == self.item_id {
del here;
report {"deleted": self.item_id};
}
}
}
Pattern 2: Search Walker#
node Item {
has id: str;
has name: str;
}
def calculate_relevance(item: Item, query: str) -> int {
return 1;
}
walker:priv SearchItems {
has query: str;
has matches: list = [];
can start with `root entry {
visit [-->];
}
can check with Item entry {
if self.query.lower() in here.name.lower() {
self.matches.append({
"id": here.id,
"name": here.name,
"score": calculate_relevance(here, self.query)
});
}
}
can finish with `root exit {
# Sort by relevance
self.matches.sort(key=lambda x: any: x["score"], reverse=True);
report self.matches;
}
}
Pattern 3: Hierarchical Traversal#
walker:priv GetTree {
can build_tree(node: any) -> dict {
children = [];
for child in [node -->] {
children.append(self.build_tree(child));
}
return {
"id": node.id,
"name": node.name,
"children": children
};
}
can start with `root entry {
tree = self.build_tree(here);
report tree;
}
}
Pattern 4: Aggregate Walker#
walker:priv GetStats {
has total: int = 0;
has completed: int = 0;
can count with `root entry {
visit [-->];
}
can tally with Todo entry {
self.total += 1;
if here.completed {
self.completed += 1;
}
}
can summarize with `root exit {
report {
"total": self.total,
"completed": self.completed,
"pending": self.total - self.completed,
"completion_rate": (self.completed / self.total * 100) if self.total > 0 else 0
};
}
}
Edge Operations#
Define Edge Types#
Create Typed Edges#
node Todo {
has id: str;
has title: str;
}
edge ChildOf {
has order: int = 0;
}
walker ProcessWithEdge {
can setup with `root entry {
parent = here ++> Todo(id="1", title="Parent");
child = here ++> Todo(id="2", title="Child");
# Connect with edge type and data
parent[0] +>: ChildOf(order=1) :+> child[0];
}
# Access edge data during traversal
can process with Todo entry {
# Access via edges in traversal filter
print(f"Processing Todo: {here.title}");
}
}
Best Practices#
- Use specific entry points -
with Todo entryis more efficient than genericwith entry - Accumulate then report - Collect data during traversal, report once at exit
- Handle empty graphs - Always check if traversal found anything
- Use meaningful node types - Makes code self-documenting
- Keep walkers focused - One walker, one responsibility