ppforest2 v0.1.0
Projection Pursuit Decision Trees and Random Forests
Loading...
Searching...
No Matches
Extending: Custom Visitors

ppforest2 uses the Visitor pattern (double dispatch) to traverse trees and models without dynamic_cast. You can add new traversal logic — statistics, export formats, analysis — by implementing a visitor, without modifying the model types.

Two visitor interfaces are available:

Interface Dispatches over Use when
TreeNode::Visitor TreeCondition (split), TreeResponse (leaf) You need to walk the tree structure
Model::Visitor Tree, Forest You need to handle trees and forests differently

How double dispatch works

Each visitable class has an accept() method that calls the appropriate visit() overload on the visitor:

// 1. Caller invokes accept() on a node (via base pointer):
node->accept(visitor);
// 2. The concrete node type calls visit(*this):
void TreeCondition::accept(TreeNode::Visitor& v) const { v.visit(*this); }
void TreeResponse::accept(TreeNode::Visitor& v) const { v.visit(*this); }
// 3. The visitor's concrete visit() method runs:
void MyVisitor::visit(const TreeCondition& cond) { ... }
void MyVisitor::visit(const TreeResponse& resp) { ... }

This resolves both the node type and the visitor type at runtime without any casts.

Implementing a TreeNode::Visitor

Interface to implement

struct TreeNode::Visitor {
virtual void visit(const TreeCondition& condition) = 0;
virtual void visit(const TreeResponse& response) = 0;
};

TreeCondition (internal split node) provides:

  • projector — the projection vector (p) used at this split
  • threshold — the split threshold in projected space
  • lower / upper — child nodes (TreeNode::Ptr)
  • groups — set of group labels reachable from this node
  • pp_index_value — the PP index value achieved at this split
  • training_spec — the TrainingSpec used to build this subtree

TreeResponse (leaf node) provides:

  • value — the predicted group label

Traversal pattern

To walk the full tree, recursively call accept() on child nodes inside your visit(TreeCondition&) method:

void visit(const TreeCondition& cond) override {
// ... process this split node ...
cond.lower->accept(*this); // recurse left
cond.upper->accept(*this); // recurse right
}
void visit(const TreeResponse& resp) override {
// ... process this leaf node (base case) ...
}

Example: computing tree depth

namespace ppforest2 {
struct DepthVisitor : public TreeNode::Visitor {
int max_depth = 0;
int current_depth = 0;
void visit(const TreeCondition& cond) override {
++current_depth;
if (current_depth > max_depth)
max_depth = current_depth;
cond.lower->accept(*this);
cond.upper->accept(*this);
--current_depth; // backtrack
}
void visit(const TreeResponse&) override {
++current_depth;
if (current_depth > max_depth)
max_depth = current_depth;
--current_depth;
}
};
} // namespace ppforest2
Definition Benchmark.hpp:22

Usage:

DepthVisitor visitor;
tree.root->accept(visitor);
int depth = visitor.max_depth;

Example: counting leaves

struct LeafCountVisitor : public TreeNode::Visitor {
int count = 0;
void visit(const TreeCondition& cond) override {
cond.lower->accept(*this);
cond.upper->accept(*this);
}
void visit(const TreeResponse&) override {
++count;
}
};

Usage:

LeafCountVisitor visitor;
tree.root->accept(visitor);
int leaves = visitor.count;

Implementing a Model::Visitor

Model::Visitor dispatches over Tree and Forest, useful when you need to handle them differently (e.g. serialisation, summary statistics).

Interface to implement

struct Model::Visitor {
virtual void visit(const Tree& tree) = 0;
virtual void visit(const Forest& forest) = 0;
};

Example: collecting all leaf labels

struct AllLabelsVisitor : public Model::Visitor {
std::set<types::Response> labels;
void visit(const Tree& tree) override {
// Use a TreeNode::Visitor to walk the tree:
struct LabelCollector : public TreeNode::Visitor {
std::set<types::Response>& labels;
LabelCollector(std::set<types::Response>& l) : labels(l) {}
void visit(const TreeCondition& c) override {
c.lower->accept(*this);
c.upper->accept(*this);
}
void visit(const TreeResponse& r) override {
labels.insert(r.value);
}
};
LabelCollector collector(labels);
tree.root->accept(collector);
}
void visit(const Forest& forest) override {
for (const auto& bt : forest.trees)
visit(static_cast<const Tree&>(*bt));
}
};

Usage:

AllLabelsVisitor visitor;
model.accept(visitor);
// visitor.labels contains all group labels in the model

Existing visitors as reference

Visitor File Purpose
VIVisitor models/VIVisitor.hpp Accumulates projection-based variable importance at each split
JsonNodeVisitor serialization/Json.hpp Serialises tree nodes to JSON
JsonModelVisitor serialization/Json.hpp Serialises Tree/Forest to JSON
NodeDataVisitor models/Visualization.hpp Routes observations through the tree, collects per-node histograms
BoundaryVisitor models/Visualization.hpp Projects decision boundaries into 2D for plotting
RegionVisitor models/Visualization.hpp Computes convex decision region polygons via Sutherland-Hodgman clipping

Conventions

  • Single-use — instantiate a fresh visitor for each traversal. Do not reuse a visitor across multiple trees without resetting its state.
  • Mutable accumulators — store results in public mutable members (e.g. int count, json result, std::vector<NodeData> nodes). Read them after accept() returns.
  • Not thread-safe — visitors carry mutable state and should not be shared across threads. Use one visitor per thread if needed.
  • Recursive traversal — call cond.lower->accept(*this) and cond.upper->accept(*this) inside visit(TreeCondition&) to walk the full tree. Omit these calls if you only need to inspect the root.
See also
TreeNode::Visitor, Model::Visitor, VIVisitor, TreeCondition, TreeResponse