ExamplesBy LevelBy TopicLearning Paths
434 Fundamental

434: Domain-Specific Languages with Macros

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "434: Domain-Specific Languages with Macros" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Some problem domains have natural syntaxes that are more readable and less error-prone than general-purpose code. Key difference from OCaml: 1. **Token matching**: Rust DSL macros parse token streams with pattern matching; OCaml uses formal grammars (Menhir) or combinator parsers.

Tutorial

The Problem

Some problem domains have natural syntaxes that are more readable and less error-prone than general-purpose code. SQL queries, HTML templates, configuration languages, and test specifications benefit from DSL syntax. macro_rules! can implement simple DSLs within Rust source: query!(SELECT name, age FROM users WHERE "active = true") is safer and more readable than string concatenation. The macro validates structure at compile time (correct SQL keywords, table identifiers) and generates the underlying representation.

DSL macros appear in sqlx's typed queries, html! in Yew, css! in Stylist, and testing DSLs like rstest's table-driven tests.

🎯 Learning Outcomes

  • • Understand how macro_rules! can parse domain-specific syntax using identifier and expression fragments
  • • Learn the SELECT $cols FROM $table WHERE $cond pattern matching technique
  • • See how stringify!($ident) converts macro identifiers to strings for query building
  • • Understand the limitations: macros parse tokens, not semantics (no SQL schema validation)
  • • Learn when DSL macros are appropriate (stable syntax, compile-time structure checking)
  • Code Example

    #![allow(clippy::all)]
    //! Domain Specific Languages with Macros
    //!
    //! Creating mini-languages in macros.
    
    /// SQL-like query builder.
    #[macro_export]
    macro_rules! query {
        (SELECT $($col:ident),+ FROM $table:ident) => {{
            let cols: Vec<&str> = vec![$(stringify!($col)),+];
            format!("SELECT {} FROM {}", cols.join(", "), stringify!($table))
        }};
        (SELECT $($col:ident),+ FROM $table:ident WHERE $cond:expr) => {{
            let cols: Vec<&str> = vec![$(stringify!($col)),+];
            format!("SELECT {} FROM {} WHERE {}", cols.join(", "), stringify!($table), $cond)
        }};
    }
    
    /// HTML-like builder.
    #[macro_export]
    macro_rules! html {
        ($tag:ident { $content:expr }) => {
            format!("<{}>{}</{}>", stringify!($tag), $content, stringify!($tag))
        };
    }
    
    #[cfg(test)]
    mod tests {
        #[test]
        fn test_query_simple() {
            let q = query!(SELECT name, age FROM users);
            assert!(q.contains("SELECT"));
            assert!(q.contains("name"));
            assert!(q.contains("users"));
        }
    
        #[test]
        fn test_query_where() {
            let q = query!(SELECT id FROM items WHERE "active = true");
            assert!(q.contains("WHERE"));
            assert!(q.contains("active"));
        }
    
        #[test]
        fn test_html_div() {
            let h = html!(div { "Hello" });
            assert_eq!(h, "<div>Hello</div>");
        }
    
        #[test]
        fn test_html_span() {
            let h = html!(span { "World" });
            assert_eq!(h, "<span>World</span>");
        }
    
        #[test]
        fn test_query_multiple_cols() {
            let q = query!(SELECT a, b, c FROM table);
            assert!(q.contains("a, b, c"));
        }
    }

    Key Differences

  • Token matching: Rust DSL macros parse token streams with pattern matching; OCaml uses formal grammars (Menhir) or combinator parsers.
  • Compile-time validation: Both can validate DSL syntax at compile time; Rust through macro arm matching, OCaml through PPX.
  • Runtime DSLs: OCaml's parser ecosystem (angstrom, sedlex) handles runtime DSL parsing elegantly; Rust uses nom, pest, or chumsky.
  • Expressiveness: Complex DSLs with context-sensitive rules exceed macro_rules! capability; both fall back to proc macros / PPX for complex cases.
  • OCaml Approach

    OCaml implements DSLs through its parsing infrastructure. The menhir parser generator creates parsers for arbitrary grammars. PPX extensions enable [%sql "SELECT ..."] syntax validated by the PPX. angstrom provides parser combinators for runtime DSL parsing. OCaml's quotations (in camlp5) support <:expr< ... >> syntax for embedding OCaml-like expressions — similar to Rust's quote! but for OCaml ASTs.

    Full Source

    #![allow(clippy::all)]
    //! Domain Specific Languages with Macros
    //!
    //! Creating mini-languages in macros.
    
    /// SQL-like query builder.
    #[macro_export]
    macro_rules! query {
        (SELECT $($col:ident),+ FROM $table:ident) => {{
            let cols: Vec<&str> = vec![$(stringify!($col)),+];
            format!("SELECT {} FROM {}", cols.join(", "), stringify!($table))
        }};
        (SELECT $($col:ident),+ FROM $table:ident WHERE $cond:expr) => {{
            let cols: Vec<&str> = vec![$(stringify!($col)),+];
            format!("SELECT {} FROM {} WHERE {}", cols.join(", "), stringify!($table), $cond)
        }};
    }
    
    /// HTML-like builder.
    #[macro_export]
    macro_rules! html {
        ($tag:ident { $content:expr }) => {
            format!("<{}>{}</{}>", stringify!($tag), $content, stringify!($tag))
        };
    }
    
    #[cfg(test)]
    mod tests {
        #[test]
        fn test_query_simple() {
            let q = query!(SELECT name, age FROM users);
            assert!(q.contains("SELECT"));
            assert!(q.contains("name"));
            assert!(q.contains("users"));
        }
    
        #[test]
        fn test_query_where() {
            let q = query!(SELECT id FROM items WHERE "active = true");
            assert!(q.contains("WHERE"));
            assert!(q.contains("active"));
        }
    
        #[test]
        fn test_html_div() {
            let h = html!(div { "Hello" });
            assert_eq!(h, "<div>Hello</div>");
        }
    
        #[test]
        fn test_html_span() {
            let h = html!(span { "World" });
            assert_eq!(h, "<span>World</span>");
        }
    
        #[test]
        fn test_query_multiple_cols() {
            let q = query!(SELECT a, b, c FROM table);
            assert!(q.contains("a, b, c"));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        #[test]
        fn test_query_simple() {
            let q = query!(SELECT name, age FROM users);
            assert!(q.contains("SELECT"));
            assert!(q.contains("name"));
            assert!(q.contains("users"));
        }
    
        #[test]
        fn test_query_where() {
            let q = query!(SELECT id FROM items WHERE "active = true");
            assert!(q.contains("WHERE"));
            assert!(q.contains("active"));
        }
    
        #[test]
        fn test_html_div() {
            let h = html!(div { "Hello" });
            assert_eq!(h, "<div>Hello</div>");
        }
    
        #[test]
        fn test_html_span() {
            let h = html!(span { "World" });
            assert_eq!(h, "<span>World</span>");
        }
    
        #[test]
        fn test_query_multiple_cols() {
            let q = query!(SELECT a, b, c FROM table);
            assert!(q.contains("a, b, c"));
        }
    }

    Deep Comparison

    OCaml vs Rust: macro dsl

    See example.rs and example.ml for side-by-side implementations.

    Key Points

  • Rust macros operate at compile time
  • OCaml uses ppx for similar metaprogramming
  • Both languages support powerful code generation
  • Rust's macro_rules! is built into the language
  • OCaml's approach requires external tooling
  • Exercises

  • Route DSL: Implement route!(GET "/users/{id}" => handler_fn) that generates a Route { method: Method::Get, path: "/users/{id}", handler: handler_fn } struct. Parse GET/POST/PUT/DELETE as ident fragments.
  • Test table DSL: Create test_cases!( add | a | b | result | 1 | 2 | 3, 4 | 5 | 9 ) that generates individual test functions for each row, asserting add(a, b) == result.
  • Config DSL: Implement config!{ timeout: 30, host: "localhost", port: 8080 } producing a Config { timeout: 30, host: "localhost".to_string(), port: 8080 } value, handling integer and string literals with appropriate type coercion.
  • Open Source Repos