Some routing problems involve paths with negative cost. Currency exchange arbitrage is the classic example — a sequence of trades can return more than you started with, expressible as a negative-cost cycle in a graph. Refund flows in finance, time-shifted scheduling where doing a task earlier saves money, and constraint systems where some inputs offset others all share this shape. The standard shortest-path algorithm fails on negative costs — it can be tricked into committing to a path that looks short locally but is actually long. The Bellman-Ford algorithm handles negative costs at a higher computational cost — work grows with the network size times the number of edges, not the logarithm of nodes. It also detects when a negative-cost cycle exists, which is the signal for "your input data has an arbitrage opportunity." Rust's petgraph implements it as a single function call.
09
Routing when paths can have negative costs
Negative-cost paths break the standard router. The fix is slower, but it stays cheap.
- Currency arbitrage as a graph: each edge weight is
-ln(rate).bellman_fordeither returns the shortest-path table or signals a negative cycle — and the negative cycle is the arbitrage opportunity, not an error.use petgraph::algo::bellman_ford; use petgraph::graph::{DiGraph, NodeIndex}; fn detect_arbitrage(fx: &DiGraph<&str, f32>, from: NodeIndex) -> Result<(), &'static str> { match bellman_ford(fx, from) { Ok(_paths) => Ok(()), Err(_) => Err("negative cycle — arbitrage opportunity exists"), } } -
matchis Rust's pattern-matching expression — it picks one branch out of several based on the shape of a value. When that value is an enum (a type with named variants), each branch handles one variant, and the compiler refuses to compile until every variant is covered. This exhaustiveness check is the safety feature: you cannot forget a case. Each arm can also bind the variant's payload —Ok(x)pulls the success value out intox,Err(e)pulls the error out intoe. The underscore_is a wildcard pattern that matches anything and binds nothing.// An enum has named variants, each optionally carrying data. enum Direction { North, South, East, West } match d { Direction::North => "up", Direction::South => "down", Direction::East => "right", Direction::West => "left", } // Delete any arm and the compiler refuses — exhaustiveness check. // Variants can carry data; match arms destructure it out. enum Shape { Circle(f64), // radius Rect(f64, f64), // width, height } let area = match s { Shape::Circle(r) => 3.1416 * r * r, Shape::Rect(w, h) => w * h, }; // _ is the wildcard — matches anything, binds nothing. match d { Direction::North => "up", _ => "not north", } -
Option<T>is the standard library's "this might be a value, or might be nothing." Two variants:Some(T)carries aT,Nonecarries nothing. Rust usesOptioneverywhere a more permissive language would usenull— looking up a key in aHashMapreturnsOption<&V>, the first element of a slice isOption<&T>, finding a substring isOption<usize>. The compiler will not let you read the inner value without first handling theNonecase, which is the whole point.enum Option<T> { Some(T), None, } let scores: HashMap<&str, i32> = HashMap::from([("alice", 90), ("bob", 75)]); let alice = scores.get("alice"); // Some(&90) let chad = scores.get("chad"); // None match alice { Some(score) => println!("alice got {}", score), None => println!("alice not in the map"), } // Common shorthands instead of writing match by hand: let s = alice.copied().unwrap_or(0); // value or fallback let doubled = alice.map(|x| x * 2); // transform if Some let exists = alice.is_some(); // → bool -
Result<T, E>is the standard library's "operation might succeed with aT, or fail with anE." Two variants:Ok(T)for success,Err(E)for failure. Reach forResultwhen the caller needs to know why something failed (parsing, I/O, anything that can go wrong in interesting ways); reach forOptionwhen the only outcomes are "got it" and "didn't." On this page,bellman_fordreturns aResult—Okcarries the table of shortest distances,Errsignals a negative cycle (no shortest path exists because you could loop the cycle forever). The wrapper here re-packages the error as a plain string because the caller does not need the full detail.enum Result<T, E> { Ok(T), Err(E), } // Typical use — parse a string as an integer. let parsed: Result<i32, _> = "42".parse(); match parsed { Ok(n) => println!("got the number {}", n), Err(e) => println!("not a number: {}", e), } // Page 9's use — bellman_ford returns Result<Paths, NegativeCycle>. match bellman_ford(fx, from) { Ok(_paths) => Ok(()), // no arbitrage Err(_) => Err("negative cycle — arbitrage exists"), } // Rule of thumb: // Option<T> ← "is the value here or not?" // null, missing key, no match, empty list // Result<T, E> ← "did the operation work, and if not, why?" // parsing, I/O, math errors, validation failures