ExamplesBy LevelBy TopicLearning Paths
278 Intermediate

Sierpinski Triangle — Recursive ASCII Art

Math/Recursion

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

  • • Recursive string generation with collect and concat
  • • Translating OCaml's List.map to Rust's .iter().map().collect()
  • • Using format! for string padding and duplication
  • • Fold-based iterative alternative to recursion
  • 🦀 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 operations: OCaml's String.make n ' ' ^ s becomes format!("{}{}", " ".repeat(pad), s) — both readable, different idioms
  • List concatenation: OCaml's top @ bottom is O(n); Rust's [top, bottom].concat() allocates a new Vec
  • Bit shift: Both use 1 lsl n / 1 << n for powers of 2 — identical semantics
  • Mutability: OCaml's recursion is naturally immutable; Rust's fold version uses an accumulator that's moved (not mutated) each iteration
  • OCaml 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);
            }
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptOCamlRust
    Return typestring list (implicitly, via go)Vec<String>
    Line paddingString.make n ' ' ^ sformat!("{}{}", " ".repeat(pad), s)
    Line duplications ^ " " ^ sformat!("{} {}", s, s)
    List concattop @ bottom[top, bottom].concat()
    Bit shift1 lsl n1 << n

    Key Insights

  • Structural recursion translates directly: The recursive decomposition (base case → pad top → duplicate bottom → concatenate) maps 1:1 from OCaml to Rust, showing that functional algorithms transfer cleanly across languages
  • String building patterns: OCaml uses ^ for concatenation; Rust's format! macro is more flexible and avoids intermediate allocations when building complex strings
  • List vs Vec for recursive generation: OCaml's linked list has O(1) prepend and O(n) append (@); Rust's Vec has O(1) push and O(n) concat — similar asymptotic cost, different cache behavior
  • Inner function vs top-level: OCaml uses let 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
  • Fold as iteration: The (0..n).fold(...) version shows how recursion over a counter naturally becomes fold over a range — Rust's range iterators make this particularly clean
  • When 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

  • Implement Sierpinski carpet (a 2D square fractal) using the same recursive subdivision approach, parameterized by depth.
  • Generalize the rendering function to output SVG or HTML canvas instructions instead of ASCII, so the fractal can be displayed at arbitrary resolution.
  • Implement the Koch snowflake using a string rewriting system (L-system): define production rules, apply them n times, then interpret the resulting string as drawing commands.
  • Open Source Repos