A bit of play with Rust macros

Here are a couple of macros that I wrote up on a slow evening. The first one provides a nice literal way of creating maps, and the second one mimicks Haskell’s list comprehension syntax in a rather crude manner.

My aim originally had been to:

  • have a simple if-then-else structure created in Rust, something like so:
    fn main() {
      let res = if 2 > 3 then "Yes" else "No";
    }
    

    However, this did not appear to be possible with the current macro support in Rust since a fragment of type expr (expression) can be followed only by the following sigils –=> ; ,. So much for that.

  • and secondly, to be able to replicate Haskell’s list comprehension syntax in its entirety, allowing for any possible generator. However, I then realised that Rust’s macro system also did not support actual evaluation of code during expansion like Common Lisp or Scheme/Racket does, even though it does work on the actual AST and not merely text. So out that went through the window as well.

So here’s the first macro demo:

use std::collections::HashMap;

macro_rules! my_map {
    ($($key: expr => $val: expr)*) => {
        {
            let mut map = HashMap::new();

            $(
                map.insert($key, $val);
            )*

          map
        }
    };
}

fn main() {
    let my_map = my_map!{
        1 => "One"
        2 => "Two"
        3 => "Three"
        4 => "Four"
        5 => "Five"
    };

    println!("{:?}", my_map);
}

Nothing fancy here, but you must say that it does look nicer, almost Ruby-esque! Here’s the code run:

Macushla:List_Comprehension_Macro z0ltan$ ./map
{2: "Two", 1: "One", 3: "Three", 5: "Five", 4: "Four"}

And here’s the poor man’s list comprehension that is not only limited in scope, but also entirely inflexible in its syntax (I just couldn’t be bothered tinkering with it for little return):

macro_rules! compr {
    ($id1: ident | $id2: ident <- [$start: expr ; $end: expr] , $cond: expr) => {
        {
            let mut vec = Vec::new();

            for num in $start..$end + 1 {
                if $cond(num) {
                    vec.push(num);
                }
            }

            vec
        }  
    };
}

fn even(x: i32) -> bool {
    x%2 == 0
}

fn odd(x: i32) -> bool {
    x%2 != 0
}

fn main() {
    let evens = compr![x | x <- [1;10], even];
    println!("{:?}", evens);

    let odds = compr![y | y <- [1;10], odd];
    println!("{:?}", odds);
}

As you can see, the ident fragments are completely for show, I cannot use .. in the generator (again due to restrictions on what can follow a expr fragment), and the guards don’t even check against the supposed identifier that’s supposed to be collecting the results into the final list/vector! Anyway, here’s the code run output:

Macushla:List_Comprehension_Macro z0ltan$ rustc list_compr.rs
Macushla:List_Comprehension_Macro z0ltan$ ./list_compr
[2, 4, 6, 8, 10]
[1, 3, 5, 7, 9]

Not too shabby, eh? In all seriousness though, while Rust’s macros are a big improvement over the debacle that is C’s macro system, the surface similarities to Racket’s powerful macro system ends right there – on the surface. As it stands now, there are far too many restrictions on the macro system for it to be considered a viable way of extending the syntax of the language, or even creating new languages (as in the Racket world).

EDIT: The list comprehension macro can actually be improved by using tt for the start and end of the range – this also allows .. to be used much in the Haskell way. Moreover, now we can actually simulate the exact Haskell syntax (for this very specific case, of course), and also make use of the identS as well.

Here’s the new code, but I’m keeping the old code up to remind myself that I can be a doddering idiot at times!

Updated macro:

macro_rules! compr {
    ($id1:ident | $id2:ident <- [$start:tt..$end:tt] , $cond:tt $id3:ident) => {
        {
            let mut vec = Vec::new();
 
            for $id1 in $start..$end + 1 {
                if $cond($id3) {
                    vec.push($id2);
                }
            }
 
            vec
        }  
    };
}
 
fn even(x: i32) -> bool {
    x%2 == 0
}
 
fn odd(x: i32) -> bool {
    x%2 != 0
}
 
fn main() {
    let evens = compr![x | x <- [1..10], even x];
    println!("{:?}", evens);
 
    let odds = compr![y | y <- [1..10], odd y];
    println!("{:?}", odds);
}

And a run just to make sure it’s working:

Macushla:List_Comprehension_Macro z0ltan$ rustc list_compr.rs
Macushla:List_Comprehension_Macro z0ltan$ ./list_compr
[2, 4, 6, 8, 10]
[1, 3, 5, 7, 9]

That’s much better!

Advertisements
A bit of play with Rust macros

A small vector initialisation macro in Rust

Learning Rust’s macros was almost a sort of déjà vu for me. It is remarkably similar to the new-fangled macro system used in Racket. In the first place, Racket macros are themselves quite different (syntactically and semantically) from Common Lisp macros (which I love, but I have to admit that Racket macros are much more user-friendly), and so it was quite surprising to see Rust’s macro-rules! are almost identical to Racket’s syntax-rules, including the Pattern Matching, and of course, hygiene.

I doubt that’s an historical accident, of course, but the funny bit is that it only adds to Rust’s deep heritage of borrowing not only semantics, but also syntax from other languages (I have already seen traces of SML and Haskell, and now Racket). Nevertheless, the best part for me was that it’s helping me pick up Rust’s macros quite quickly and easily (not an expert by any means), including recursive macros (which are, admittedly, rather difficult to grok in Common Lisp).

So in this short blog, I wanted to post a small macro that I wrote as part of my ongoing study of Rust. It is a simple vector initialisation macro. All this macro does is to take in a binding of the form:

let some_vec = init_vec![size; init_value];

where size determines how many elements we want in the vector, and init_value determines what value the entire vector will be initialised with.

At this juncture, it would be probably be wise to mention another aspect of Rust’s macros that pleasantly surprised me – in the macro call shown above, the square brackets can actually be replaced by a pair of parentheses, or even a pair of flowery brackets (or braces as some folks call them). This is again very similar to Racket’s behaviour. However, we can mix two different styles of brackets. Just to clarify this further, the following three are identical:

let some_vec = init_vec![size; init_value];
let some_vec = init_vec!(size; init_value);
let some_vec = init_vec{size; init_value};

However, something like the following is illegal:

let some_vec = init_vec!{size; init_value];

Also, before showing the code, it is equally interesting to note that the rule also applies to the definition of the macro itself. Instead of flowery brackets, we could as well have chosen to use parentheses to wrap the rule in. In fact, that would even be my preferred style since Common Lisp has made me more or less immune to parentheses! (only half-joking).

Here is the macro and some test code for it:

/// Here we demonstrate a macro that takes in a size, and an init value
/// and returns a vector of that size initialised with the init value.

use std::fmt::Debug;
use std::fmt::{Display, Formatter, Result};

// the main man!
macro_rules! init_vec {
	( $size: expr; $init: expr ) => { {
			let mut temp_vec = Vec::new();

			for _ in 0..$size {
				temp_vec.push($init);
			}

			temp_vec
		};
	} // block necessary here
}

// a custom structure
#[derive(Debug)]
struct Foo {
	bar: f64,
}

impl Foo {
	fn new() -> Foo {
		Foo {
			bar: 0.0,
		}
	}
}

impl Display for Foo {
	fn fmt(&self, f: &mut Formatter) -> Result {
		write!(f, "{{ bar: {} }}", self.bar)
	}
}


fn print_vec<T: Debug + Display>(name: &str, vec: &Vec<T>) {
	println!("{} = {:?}", name, vec);

	print!("The contents are... ");

	for e in vec.iter() {
		print!("{} ", *e);
	}
	println!("\n");
}


fn main() {
	let int_vec = init_vec![10; 99];
	print_vec("int_vec", &int_vec);

	let str_vec = init_vec![5; "hello"];
	print_vec("str_vec", &str_vec);
	
	let mut foo_vec = init_vec![7; Foo::new()];
	print_vec("foo_vec", &foo_vec);

	// we can also use it like any normal vector
	foo_vec[0].bar = 100.0;
	foo_vec[3].bar = 2.71828;
	foo_vec[5].bar = 3.14159;

	println!("After mutation...\n");

	print_vec("foo_vec", &foo_vec);
}

Let’s take it for a quick run:

int_vec = [99, 99, 99, 99, 99, 99, 99, 99, 99, 99]
The contents are... 99 99 99 99 99 99 99 99 99 99 

str_vec = ["hello", "hello", "hello", "hello", "hello"]
The contents are... hello hello hello hello hello 

foo_vec = [Foo { bar: 0 }, Foo { bar: 0 }, Foo { bar: 0 }, Foo { bar: 0 }, Foo { bar: 0 }, Foo { bar: 0 }, Foo { bar: 0 }]
The contents are... { bar: 0 } { bar: 0 } { bar: 0 } { bar: 0 } { bar: 0 } { bar: 0 } { bar: 0 } 

After mutation...

foo_vec = [Foo { bar: 100 }, Foo { bar: 0 }, Foo { bar: 0 }, Foo { bar: 2.71828 }, Foo { bar: 0 }, Foo { bar: 3.14159 }, Foo { bar: 0 }]
The contents are... { bar: 100 } { bar: 0 } { bar: 0 } { bar: 2.71828 } { bar: 0 } { bar: 3.14159 } { bar: 0 } 

Excellent! So it all works pretty much as expected. The code probably merits a bit of explanation – in the macro, we simply define a rule which expects an expression ( $size: expr), followed by a semi-colon, followed by another expression ($init: expr).

Note that expr has a special meaning in Rust. It is what is called a “designator” and defines the type of the data being passed in. There are various such designators available (ident for identifiers. type for types, etc. Refer to the documentation for further details).

So the whole ( $size: expr; $init: expr ) denotes the pattern that, if matched, leads to the logic on the right-hand side of the =>. Now, the right hand-side is where all the action takes place. All we do there is to create a temporary vector, and then insert as many init values as size inside the vector, and finally return the vector. Since there are multiple expression/statements in the rule, we safely ensconce them within a block using the flowery brackets. Note that there is really no type-checking done inside to see if size is a valid numeric type. This may not be possible since macro-expansion should happen very early in the compilation phase. However, I might be wrong (as many times before!), and if I come across such means, I will update this blog accordingly.

So, it was a quick little fun exercise, and it makes Rust so much more appealing to me now especially since it’s been some time since I’ve played around with macros in Common Lisp. Maybe one of these days, eh?

A small vector initialisation macro in Rust