Sierpinski Triangle — Recursive ASCII Art
Tutorial Video
Text description (accessibility)
This video demonstrates the "Sierpinski Triangle — Recursive ASCII Art" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Math/Recursion. Generate a Sierpinski triangle of order N as ASCII art. Key difference from OCaml: 1. **String operations:** OCaml's `String.make n ' ' ^ s` becomes `format!("{}{}", " ".repeat(pad), s)` — both readable, different idioms
Tutorial
The Problem
Generate a Sierpinski triangle of order N as ASCII art. Each order doubles the number of lines: order 0 is a single *, order 1 has 2 lines, order N has 2^N lines.
🎯 Learning Outcomes
List.map to Rust's .iter().map().collect()format! for string padding and duplication🦀 The Rust Way
Rust mirrors the recursive structure exactly, using Vec<String> instead of string list. .iter().map().collect() replaces List.map, and [top, bottom].concat() replaces @. An iterative version uses fold to build up from order 0.
Code Example
pub fn sierpinski(n: u32) -> Vec<String> {
if n == 0 {
return vec!["*".to_string()];
}
let prev = sierpinski(n - 1);
let width = (1 << n) - 1;
let top: Vec<String> = prev.iter()
.map(|s| {
let pad = (width - s.len()) / 2;
format!("{}{}", " ".repeat(pad), s)
})
.collect();
let bottom: Vec<String> = prev.iter()
.map(|s| format!("{} {}", s, s))
.collect();
[top, bottom].concat()
}Key Differences
String.make n ' ' ^ s becomes format!("{}{}", " ".repeat(pad), s) — both readable, different idiomstop @ bottom is O(n); Rust's [top, bottom].concat() allocates a new Vec1 lsl n / 1 << n for powers of 2 — identical semanticsOCaml Approach
OCaml builds the triangle recursively: base case is ["*"], then each level maps pad over previous lines for the top half and duplicates lines (s ^ " " ^ s) for the bottom half. List.map and list concatenation (@) make this concise.
Full Source
#![allow(clippy::all)]
/// Generate the lines of a Sierpinski triangle of given order.
///
/// # Recursive approach (mirrors OCaml)
///
/// At order 0, we have a single `"*"`. Each higher order:
/// 1. Takes the previous triangle lines
/// 2. Centers them (pads with spaces) for the top half
/// 3. Duplicates each line side-by-side for the bottom half
pub fn sierpinski(n: u32) -> Vec<String> {
if n == 0 {
return vec!["*".to_string()];
}
let prev = sierpinski(n - 1);
// Width used for centering the top half.
// This matches OCaml's `1 lsl n - 1` = (1 << n) - 1
let width = (1 << n) - 1;
// Top: center each line from the previous order
let top: Vec<String> = prev
.iter()
.map(|s| {
let pad = (width - s.len()) / 2;
format!("{}{}", " ".repeat(pad), s)
})
.collect();
// Bottom: duplicate each line with a space between
let bottom: Vec<String> = prev.iter().map(|s| format!("{} {}", s, s)).collect();
[top, bottom].concat()
}
/// Iterative version — builds the triangle bottom-up using fold.
pub fn sierpinski_iter(n: u32) -> Vec<String> {
(1..=n).fold(vec!["*".to_string()], |prev, i| {
let width = (1 << i) - 1;
let top: Vec<String> = prev
.iter()
.map(|s| {
let pad = (width - s.len()) / 2;
format!("{}{}", " ".repeat(pad), s)
})
.collect();
let bottom: Vec<String> = prev.iter().map(|s| format!("{} {}", s, s)).collect();
[top, bottom].concat()
})
}
/// Print the Sierpinski triangle to stdout.
pub fn print_sierpinski(n: u32) {
for line in sierpinski(n) {
println!("{}", line);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_order_zero() {
let result = sierpinski(0);
assert_eq!(result, vec!["*"]);
}
#[test]
fn test_order_one() {
let result = sierpinski(1);
assert_eq!(result.len(), 2);
// Order 1: top is "*" (no pad since width=1), bottom is "* *"
assert_eq!(result[0], "*");
assert_eq!(result[1], "* *");
}
#[test]
fn test_order_two() {
let result = sierpinski(2);
assert_eq!(result.len(), 4);
// Order 2: width=3, centering the order-1 triangle
assert_eq!(result[0], " *");
assert_eq!(result[1], "* *");
assert_eq!(result[2], "* *");
assert_eq!(result[3], "* * * *");
}
#[test]
fn test_order_four_line_count() {
// Order n should have 2^n lines
let result = sierpinski(4);
assert_eq!(result.len(), 16); // 2^4 = 16
}
#[test]
fn test_iterative_matches_recursive() {
for n in 0..6 {
assert_eq!(sierpinski(n), sierpinski_iter(n), "Mismatch at order {}", n);
}
}
#[test]
fn test_bottom_row_all_stars() {
// The bottom row of order n contains 2^n asterisks separated by spaces
let result = sierpinski(3);
let bottom = result.last().unwrap();
let star_count = bottom.chars().filter(|&c| c == '*').count();
assert_eq!(star_count, 8); // 2^3 = 8 asterisks
}
#[test]
fn test_first_line_single_star() {
// The first line of any order > 0 should contain exactly one '*'
for n in 1..5 {
let result = sierpinski(n);
let star_count = result[0].chars().filter(|&c| c == '*').count();
assert_eq!(star_count, 1, "Order {} first line should have 1 star", n);
}
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_order_zero() {
let result = sierpinski(0);
assert_eq!(result, vec!["*"]);
}
#[test]
fn test_order_one() {
let result = sierpinski(1);
assert_eq!(result.len(), 2);
// Order 1: top is "*" (no pad since width=1), bottom is "* *"
assert_eq!(result[0], "*");
assert_eq!(result[1], "* *");
}
#[test]
fn test_order_two() {
let result = sierpinski(2);
assert_eq!(result.len(), 4);
// Order 2: width=3, centering the order-1 triangle
assert_eq!(result[0], " *");
assert_eq!(result[1], "* *");
assert_eq!(result[2], "* *");
assert_eq!(result[3], "* * * *");
}
#[test]
fn test_order_four_line_count() {
// Order n should have 2^n lines
let result = sierpinski(4);
assert_eq!(result.len(), 16); // 2^4 = 16
}
#[test]
fn test_iterative_matches_recursive() {
for n in 0..6 {
assert_eq!(sierpinski(n), sierpinski_iter(n), "Mismatch at order {}", n);
}
}
#[test]
fn test_bottom_row_all_stars() {
// The bottom row of order n contains 2^n asterisks separated by spaces
let result = sierpinski(3);
let bottom = result.last().unwrap();
let star_count = bottom.chars().filter(|&c| c == '*').count();
assert_eq!(star_count, 8); // 2^3 = 8 asterisks
}
#[test]
fn test_first_line_single_star() {
// The first line of any order > 0 should contain exactly one '*'
for n in 1..5 {
let result = sierpinski(n);
let star_count = result[0].chars().filter(|&c| c == '*').count();
assert_eq!(star_count, 1, "Order {} first line should have 1 star", n);
}
}
}
Deep Comparison
OCaml vs Rust: Sierpinski Triangle
Side-by-Side Code
OCaml
let sierpinski n =
let rec go n =
if n = 0 then ["*"]
else
let prev = go (n - 1) in
let width = 1 lsl n - 1 in
let pad s = String.make ((width - String.length s) / 2) ' ' ^ s in
let top = List.map pad prev in
let bottom = List.map (fun s -> s ^ " " ^ s) prev in
top @ bottom
in
List.iter print_endline (go n)
Rust (idiomatic)
pub fn sierpinski(n: u32) -> Vec<String> {
if n == 0 {
return vec!["*".to_string()];
}
let prev = sierpinski(n - 1);
let width = (1 << n) - 1;
let top: Vec<String> = prev.iter()
.map(|s| {
let pad = (width - s.len()) / 2;
format!("{}{}", " ".repeat(pad), s)
})
.collect();
let bottom: Vec<String> = prev.iter()
.map(|s| format!("{} {}", s, s))
.collect();
[top, bottom].concat()
}
Rust (functional/iterative fold)
pub fn sierpinski_iter(n: u32) -> Vec<String> {
(0..n).fold(vec!["*".to_string()], |prev, _| {
let width = prev.last().map_or(1, |s| s.len() * 2 + 1);
let top: Vec<String> = prev.iter()
.map(|s| {
let pad = (width - s.len()) / 2;
format!("{}{}", " ".repeat(pad), s)
})
.collect();
let bottom: Vec<String> = prev.iter()
.map(|s| format!("{} {}", s, s))
.collect();
[top, bottom].concat()
})
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Return type | string list (implicitly, via go) | Vec<String> |
| Line padding | String.make n ' ' ^ s | format!("{}{}", " ".repeat(pad), s) |
| Line duplication | s ^ " " ^ s | format!("{} {}", s, s) |
| List concat | top @ bottom | [top, bottom].concat() |
| Bit shift | 1 lsl n | 1 << n |
Key Insights
^ for concatenation; Rust's format! macro is more flexible and avoids intermediate allocations when building complex strings@); Rust's Vec has O(1) push and O(n) concat — similar asymptotic cost, different cache behaviorlet rec go as an inner helper; Rust uses the public function itself as the recursion — Rust doesn't need the indirection since there's no separate printing concern(0..n).fold(...) version shows how recursion over a counter naturally becomes fold over a range — Rust's range iterators make this particularly cleanWhen to Use Each Style
Use idiomatic Rust when: The recursive structure makes the algorithm clearest — fractal generation is inherently self-similar, and the recursive version communicates that directly.
Use iterative Rust when: You want to avoid stack depth concerns for large orders, or when composing with other iterator operations. The fold version keeps the same logic but avoids function call overhead.
Exercises
n times, then interpret the resulting string as drawing commands.