Many business problems that look combinatorial collapse into a matrix computation once the structure is spotted. Google's PageRank ranks web pages as the principal eigenvector of a link-graph matrix. Recommendation systems use matrix factorization to predict missing entries in a user-item matrix. Search ranking, image compression (JPEG, SVD), principal component analysis, spectral clustering, network centrality — all matrix operations. Linear algebra is in the cheap category. Matrix multiplication, eigenvalue computation, matrix factorization all run in polynomial time, and Rust has multiple production-grade libraries — nalgebra for general use, faer for high-performance dense problems, ndarray for NumPy-like array operations. When a problem can be expressed in matrix form, the engineering work is in the data preparation, not the computation.
22
When the answer is a matrix
When the problem has a matrix at its heart, the answer is in the linear algebra library — and the work is cheap.
- PageRank in twenty lines of
nalgebra— build the column-stochastic transition matrix, run power iteration, return the dominant eigenvector. The same shape solves spectral clustering, recommendation scoring, and influence ranking.use nalgebra::{DMatrix, DVector}; fn pagerank(links: &DMatrix<f64>, damping: f64, iterations: usize) -> DVector<f64> { let n = links.ncols(); let teleport = DVector::from_element(n, (1.0 - damping) / n as f64); let mut r = DVector::from_element(n, 1.0 / n as f64); for _ in 0..iterations { r = links * (&r * damping) + &teleport; } r } -
for _ in 0..iterationsis the counted-loop idiom — "run this bodyiterationstimes, no step counter needed." The range0..iterations(page 15) is an iterator yielding0, 1, 2, …, iterations - 1, and the loop binds each yielded value to the pattern on the left. Here the pattern is just_, the wildcard from page 9 that matches anything and binds nothing. Same range, two patterns:for i in 0..nwhen you actually need the step number,for _ in 0..nto make it clear you do not. The underscore is documentation for the reader as much as it is syntax for the compiler.// Counted loop — run the body N times, discard the index: for _ in 0..5 { println!("again"); } // prints "again" five times. // If you need the step number, bind it: for i in 0..5 { println!("step {}", i); // step 0, step 1, ..., step 4 } // On page 22, only the repetition matters — not the iteration index. // The _ tells the reader "I'm not using the loop variable on purpose": for _ in 0..iterations { r = links * (&r * damping) + &teleport; } // More verbose equivalents — both work, neither idiomatic: let mut k = 0; while k < iterations { r = links * (&r * damping) + &teleport; k += 1; } // For Rust, for/in is the idiomatic counted loop. Reach for while only // when the stop condition is not a simple count. - Two floating-point expressions set up PageRank's bookkeeping.
1.0 / n as f64is the initial rank every page starts with — the total rank mass is1.0and it is split evenly across allnpages, so each page starts with1/n.(1.0 - damping) / n as f64is the teleport mass — at each iteration, with probability1 - damping(typically0.15when damping is0.85), the model says a random surfer teleports to a uniformly random page, contributing(1 - damping) / nto every page's rank. In both expressions,as f64converts theusizecountnto a 64-bit float for the division — Rust never implicitly converts between integer and float types (the same rule from page 15 applied across number kinds, not just across integer widths). Operator precedence:asbinds tighter than/, so the parser sees1.0 / (n as f64)and(1.0 - damping) / (n as f64).// n is a usize — Rust will not implicitly convert it to f64. let n: usize = links.ncols(); // ── Initial rank — split 1.0 of mass evenly across n pages ───── // // 1.0 / n as f64 = 1.0 / (n as f64) // // for n = 10 pages → 0.1 per page // for n = 100 pages → 0.01 per page // for n = 1,000,000 → 0.000001 per page // let mut r = DVector::from_element(n, 1.0 / n as f64); // ── Teleport mass per page ──────────────────────────────────── // // (1.0 - damping) / n as f64 = (1.0 - damping) / (n as f64) // // damping = 0.85, n = 100 → teleport = 0.15 / 100 = 0.0015 per page // let teleport = DVector::from_element(n, (1.0 - damping) / n as f64); // Operator precedence — `as` binds tighter than `/`: // // 1.0 / n as f64 parses as 1.0 / (n as f64) // (1.0 - damping) / n as f64 parses as (1.0 - damping) / (n as f64) // One iteration of the PageRank update: // // r_new = links · (r · damping) + teleport // ──────────────────── ──────── // redistribute damping add a flat teleport // fraction of rank along contribution to every // outbound links page // // Repeat until r stops changing (or for a fixed iteration budget).