#[derive(Debug, Clone)]
pub enum Outcome {
Approve,
Review,
Reject(String),
}
#[derive(Debug, Clone)]
pub enum Predicate {
GreaterThan { feature: String, value: f64 },
LessThan { feature: String, value: f64 },
Equals { feature: String, value: f64 },
}
#[derive(Debug, Clone)]
pub enum Node {
Leaf(Outcome),
Branch {
test: Predicate,
yes: Box<Node>,
no: Box<Node>,
},
}
impl Node {
pub fn leaf(outcome: Outcome) -> Node {
Node::Leaf(outcome)
}
pub fn branch(test: Predicate, yes: Node, no: Node) -> Node {
Node::Branch {
test,
yes: Box::new(yes),
no: Box::new(no),
}
}
}
use std::collections::HashMap;
use crate::tree::{Node, Outcome, Predicate};
pub struct Features {
values: HashMap<String, f64>,
}
impl Features {
pub fn new() -> Features {
Features { values: HashMap::new() }
}
pub fn set(mut self, name: &str, value: f64) -> Features {
self.values.insert(name.to_string(), value);
self
}
pub fn get(&self, name: &str) -> Option<f64> {
self.values.get(name).copied()
}
}
impl Predicate {
fn test(&self, features: &Features) -> bool {
match self {
Predicate::GreaterThan { feature, value } => {
features.get(feature).map_or(false, |v| v > *value)
}
Predicate::LessThan { feature, value } => {
features.get(feature).map_or(false, |v| v < *value)
}
Predicate::Equals { feature, value } => {
features.get(feature).map_or(false, |v| (v - *value).abs() < f64::EPSILON)
}
}
}
}
impl Node {
pub fn evaluate<'a>(&'a self, features: &Features) -> &'a Outcome {
match self {
Node::Leaf(outcome) => outcome,
Node::Branch { test, yes, no } => {
if test.test(features) {
yes.evaluate(features)
} else {
no.evaluate(features)
}
}
}
}
}
use crate::tree::{Node, Outcome, Predicate};
use crate::eval::Features;
fn build_credit_tree() -> Node {
Node::branch(
Predicate::GreaterThan { feature: "score".into(), value: 700.0 },
Node::leaf(Outcome::Approve),
Node::branch(
Predicate::LessThan { feature: "score".into(), value: 500.0 },
Node::leaf(Outcome::Reject("score below floor".into())),
Node::branch(
Predicate::GreaterThan { feature: "income".into(), value: 60_000.0 },
Node::leaf(Outcome::Approve),
Node::leaf(Outcome::Review),
),
),
)
}
fn main() {
let tree = build_credit_tree();
let applicant = Features::new().set("score", 620.0).set("income", 72_000.0);
let borderline = Features::new().set("score", 620.0).set("income", 40_000.0);
for (label, features) in [("applicant", &applicant), ("borderline", &borderline)] {
match tree.evaluate(features) {
Outcome::Approve => println!("{label}: approved"),
Outcome::Review => println!("{label}: needs manual review"),
Outcome::Reject(reason) => println!("{label}: rejected ({reason})"),
}
}
}
A decision tree is a naturally recursive structure: each internal node asks a question and branches into subtrees, while leaves hold a final answer. In Rust that recursion has to be made explicit because a value must have a known, finite size at compile time. This snippet models the tree with recursive enums and shows how to evaluate it against a set of feature inputs.
The Tree types tab defines the core data model. A Node is either a Leaf carrying an Outcome, or a Branch holding a Predicate plus yes and no subtrees. Because a Node can contain another Node, the enum would be infinitely sized, so each child is wrapped in Box<Node> — a heap pointer of known size that breaks the cycle. The Predicate enum captures the comparison to run against a named feature, and small helper constructors like Node::leaf and Node::branch keep call sites readable while hiding the boxing.
The Evaluator tab is where the recursion actually runs. Features is a thin newtype over a HashMap so lookups can return a typed Option<f64>. Node::evaluate walks the tree: on a Leaf it returns a reference to the stored Outcome, and on a Branch it evaluates the Predicate, then recurses into yes or no. Using &self and returning &Outcome means evaluation borrows the tree rather than cloning it, which keeps traversal cheap. Predicate::test isolates the comparison logic and treats a missing feature as false so a partially specified input never panics.
The Demo tab assembles a concrete tree with the constructors and evaluates two feature sets. Pattern matching over Outcome at the end shows how the caller consumes the result. The trade-off of this design is clarity over raw speed: boxing adds an allocation and a pointer hop per node, which is fine for configuration-sized trees but not for millions of nodes in a hot loop. For that, a flattened array-of-nodes with index children would avoid the pointer chasing. For modeling rules, scoring flows, or feature gates, the recursive-enum form reads closest to the problem.
Related snips
struct Config<'a> {
name: &'a str,
value: &'a str,
}
fn parse_config(line: &str) -> Config {
Lifetime annotations for flexible borrowing in structs
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
#[arg(short, long)]
clap for CLI argument parsing with derive macros
macro_rules! create_function {
($func_name:ident) => {
fn $func_name() {
println!("Called {}", stringify!($func_name));
}
};
Declarative macros (macro_rules!) for code generation
Share this code
Here's the card — post it anywhere.