avatarBrian Enochson

Summary

This article provides an overview of macros, iterators, and file handling in Rust programming language.

Abstract

This article discusses the use of macros, iterators, and file handling in Rust programming language. Macros are explained as variadic functions that allow for meta-programming and can be implemented using the macro_rules! macro. Iterators are explained as constructs that allow for looping through collections and can be implemented for custom types. File handling in Rust is demonstrated using the std::fs library for opening, reading, and writing files. The article also includes examples and code snippets to illustrate the concepts.

Bullet points

  • Macros are explained as variadic functions that allow for meta-programming in Rust.
  • Macros can be implemented using the macro_rules! macro.
  • Iterators are constructs that allow for looping through collections in Rust.
  • Iterators can be implemented for custom types in Rust.
  • File handling in Rust is demonstrated using the std::fs library.
  • The article includes examples and code snippets to illustrate the concepts.

Learning Rust: Part 6— Macros, Iterators and Files

Look at meta-programming with Macros, looping with Iterators and then some File Handling code.

Rust Series

Part 1 — Basic Concepts

Part 2 — Memory

Part 3 — Flow Control and Functions

Part 4 — Option / Result and Collections

Part 5 — Traits, Generics and Closures

Part 6 — Macros, Iterators and File Handling (this article)

Overview

The next article in this series goes into three more areas of the language. First we take a deep look at Macros. These allow us to implement code that generates functionality. Also, by understanding details around Macros we will find the use of the pre-supplied Macros easier to use.

We then cover iterators and the different types. This is either by calling iterator functions directly or use in a for loop. We will then see how an iterator can be implemented for our own types.

Finally, we will cover File handling and how to open, read and work with files. Let’s get into it!

Macros

This article was originally just about Iterators and Files. But it was decided to add an explanation of Macros in Rust into the content. Why is Macros an add-in to this article? Well you need to know how to use them (e.g. println! or #[derive]), but you rarely need to write your own. We will look at what Macros are, to provide an understanding. Also, we will add in a few possible cases where writing your own Macro might be worthwhile.

Macros are what is called a variadic function.

In computer science (and math), a variadic function takes an arbitrary number of arguments. For example, println! or vec! can take an arbitrary number of arguments, as determined by the format string. In Rust they are expanded at compile time allowing type safety.

To write a macro in Rust you use (ironically) a macro called macro_rules!. Once this is done, you add your macro name and then wrap the macro code in a { .. } block.

Below is a simply macro that returns a string called “Hello”. Notice when it is invoked we add the ! to the name. Just like we do with println!.

macro_rules! provide_greeting {
    () => {
        "Hello"
    };
}

fn main() {
    let greeting = provide_greeting!();
    println!("My greeting is {}", greeting);
}

Another variation, that shows how parameters to the Macro can be used.

Here we see it is a sort of match statement within the macro (but isn’t). It is determined by the compiler. If we try to pass 4 to macro the compiler will complain.

macro_rules! provide_greeting {
    (1) => {
        "Hello"
    };
    (2) => {
        "Hallo"
    };
    (3) => {
        "Bonjour"
    };
}

fn main() {
    let greeting = provide_greeting!(3);
    println!("My greeting is {}", greeting);
}

An important distinction is that a Macro is not Rust syntax. A perfectly fine Macro can also work where its implementation would not be allowed in normal Rust code. This is because of how the Macro is interpreted in the compiler lifecycle.

Look at this seeminly incorrect macro. Seems incorrect, but it is valid code and compiles and runs.

macro_rules! print_greeting {
    (SOme ARbitrary TExt) => {
        println!("Hello")
    };
    (SOme OTher ARbitrary TExt) => {
        println!("Hallo")
    };
    () => {
        println!("Bonjour");
    };
}

fn main() {
    print_greeting!(SOme ARbitrary TExt);
    print_greeting!(SOme OTher ARbitrary TExt);
    print_greeting!();
}

Obviously, SOme ARbitrary TExt is not valid Rust, it is not enclosed in quotes. But when runs it prints.

Hello
Hallo
Bonjour

This is because in this case the Macro is just responding to a token, such as SOme ARbitrary TExt, or in the previous example 1, 2 or 3.

Now this is interesting, and demonstrates a lot about Macros. But, you may still be wondering about the usefulness of this information!

Where the power and flexibility of Macros come in is that you can tell a macro that it will receive an expression, a type stmt, a type name, etc. Go to this link https://cheats.rs/#macros-attributes and you will see a list of these (also shown below).

So let’s try out with expr and make our variable work with any expression.

macro_rules! print_expr {
    ($input:expr) => {
        println!("Your expression you provided was: {:?}", $input);
    };
}

fn main() {
    print_expr!(());
    print_expr!(1);
    print_expr!(1+1);
    print_expr!(vec![5, 6, 7, 8]);
}

This prints the following.

Your expression you provided was: ()
Your expression you provided was: 1
Your expression you provided was: 2
Your expression you provided was: [5, 6, 7, 8]

So how do we pass multiple input? We use the delimiter* syntax like this.

macro_rules! print_input {
    ($($input1:tt);*) => {
        let str_output = stringify!($($input1),*);
        println!("{}", str_output);
    };
}

fn main() {
    println!("==================");
    print_input!(3434535; tegg; vvfre);
    print_input!(889fvevefvfv; d3333; fhefh5; 963);
    print_input!();
    println!("==================");
}

Notice the ;* in the input portion. This means it will receive zero or more parameters delimited by a semicolon. Also, the macro input attribute is tt, which is for tokentree.

This uses stringify which is itself a macro, it will yield an expression of type &'static str.

So a macro can write a function for you, showing the special powers a macro brings to your code.

Let’s look at the following example. First it will match on a single identifier using $name:ident, then it does the same as our previous example where it prints a semicolon delimited list of tokens.

macro_rules! print_function {
    ($name:ident, $($input1:tt);*) => {
        fn $name() {
           let str_output = stringify!($($input1),*);
           println!("{}", str_output);
        }
    };
}

fn main() {
    println!("==================");
    print_function!(print_fun, 3434535; tegg; vvfre);
    print_fun();
    print_function!(another_print_fun, 889fvevefvfv; d3333; fhefh5; 963);
    another_print_fun();
    print_function!(print_empty;);
    print_empty();
    println!("==================");
}

This prints. Notice the last one where we had to declare a comma (,) to print nothing, print_function!(print_empty,);

==================
3434535, tegg, vvfre
889fvevefvfv, d3333, fhefh5, 963

==================

If you are running this in the Rust Playground, go to Tools and the last item is Expand Macros. Select this and look at the output.

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
macro_rules! print_function {
    ($name : ident, $($input1 : tt) ; *) =>
    {
        fn $name()
        {
            let str_output = stringify! ($($input1), *) ; println!
            ("{}", str_output) ;
        }
    } ;
}

fn main() {
    { ::std::io::_print(format_args!("==================\n")); };
    fn print_fun() {
        let str_output = "3434535, tegg, vvfre";
        { ::std::io::_print(format_args!("{0}\n", str_output)); };
    }
    print_fun();
    fn another_print_fun() {
        let str_output = "889fvevefvfv, d3333, fhefh5, 963";
        { ::std::io::_print(format_args!("{0}\n", str_output)); };
    }
    another_print_fun();
    fn print_empty() {
        let str_output = "";
        { ::std::io::_print(format_args!("{0}\n", str_output)); };
    }
    print_empty();
    { ::std::io::_print(format_args!("==================\n")); };
}

Pretty cool huh! It shows the complete code within the main that is generated by the macro by the compiler. Great for learning, great for debugging.

What about uses of a macro. One real use for macro is to remove almost identical code. Look at the following example. Here we define three structs with signature struct. We would need to declare this and then impl the TryFrom trait for each like this.

#[derive(Debug)]
struct SmallSizeNumber(u32);
#[derive(Debug)]
struct MidSizeNumber(u32);
#[derive(Debug)]
struct LargeSizeNumber(u32);

impl TryFrom<u32> for SmallSizeNumber {
    type Error = &'static str;
    fn try_from(value: u32) -> Result<Self, Self::Error> {
        if value > 9 {
            Err("Must be no larger than 9")
        } else {
            Ok(Self(value))
        }
    }
}

// and implement for other two structs too

fn main() {
    println!("{:?}", SmallSizeNumber::try_from(10));
    println!("{:?}", SmallSizeNumber::try_from(9));
}

So what if we want to add other struct of MediumToLargeSize, SmallToMediumSize etc. You get the picture! We could, but would be a lot of copy and paste and error prone code to write. Macros to the rescue.

We can create a macro that takes a struct name and a max size and implement it as follows.

macro_rules! make_a_type {
    ($type:ident, $the_size:expr) => {
        #[derive(Debug)]
        struct $type(u32);
        impl TryFrom<u32> for $type {
            type Error = String;
            fn try_from(value: u32) -> Result<Self, Self::Error> {
                let the_size = $the_size;
                if value > $the_size {
                    Err(format!("Must be no larger than {the_size}"))
                } else {
                    Ok(Self(value))
                }
            }
        }
    };
}
make_a_type!(SmallSizeNumber, 9);
make_a_type!(MidSizeNumber, 99);
make_a_type!(LargeSizeNumber, 99999);


fn main() {
    println!("{:?}", SmallSizeNumber::try_from(10));
    println!("{:?}", MidSizeNumber::try_from(100));
    println!("{:?}", LargeSizeNumber::try_from(50000));
}

This prints.

Err("Must be no larger than 9")
Err("Must be no larger than 99")
Ok(LargeSizeNumber(50000))

That concludes the discussion of Macros. There was a lot in there, but I think it was sufficient to show the power of macros and also to understand a little more how they work when you use them.

Iterators

An iterator is construct that gives you its items one at a time from a collection.

When you want to use an iterator other times, you have to choose what kind.

  • .iter() for an iterator of references
  • .iter_mut() for an iterator of mutable references
  • .into_iter() for an iterator of values (not references).

Let’s see all three of these in action with a simple example.

fn main() {
    let vector_one = vec![5, 6, 7];
    let mut vector_two = vec![101, 202, 303];
    for num in vector_one.iter() {
        println!("Printing a &i32: {num}");
    }
    for num in vector_one.into_iter() { // this is same as for num in vector_one
        println!("Printing an i32: {num}");
    }
    for num in vector_two.iter_mut() {
        println!("num is {num}");
    }
    println!("vector_two: {vector_two:?}");
}

Note that vector_one become unavailable with the line for num in vector_one.into_iter() since it is of values and not references. So adding a line like this at the end would cause a problem.

println!("vector_one: {vector_one:?}");

But since iter_mut() is mutable references than we can use vector_two afterwards in the println!.

Every iterator has a method called .next(), which returns an Option. When you use an iterator, it calls .next() over and over again to see if there are more items left. If .next() returns Some (remember our discussion of Option in an earlier article), there are still some items left. If None is returned, the iteration is completed and there are no items.

What about if we want to implement an iterator for our type. This is done as we will see below. This is an example of an Order / Detail scenario where we have an order (in this case from a hardware store) and the detail items that were part of the order.

#[derive(Debug)]
struct Order {
    name: String,
    details: DetailCollection,
}

#[derive(Clone, Debug)]
struct DetailCollection(Vec<String>);

impl Order {
    fn add_detail(&mut self, detail: &str) {
        self.details.0.push(detail.to_string());
    }

    fn new(name: &str) -> Self {
        Self {
            name: name.to_string(),
            details: DetailCollection(Vec::new()),
        }
    }

    fn get_details(&self) -> DetailCollection {
        self.details.clone()
    }
}

impl Iterator for DetailCollection {
    type Item = String;
    fn next(&mut self) -> Option<String> {
        match self.0.pop() {
            Some(detail) => {
                Some(detail)
            }
            None => {
                None
            }
        }
    }
}

fn main() {
    let mut my_order = Order::new("Brian's Order");
    my_order.add_detail("20 Pieces Lumber");
    my_order.add_detail("Exterior Satin Paint");
    my_order.add_detail("Circular Saw");
    my_order.add_detail("Roofing Nails");
    for detail_item in my_order.get_details() {
        println!("Order Detail: {detail_item}");
    }
}

The iterator is implemented by line impl Iterator for DetailCollection. We just need to implement a next method, that returns an Option with either Some or None.

This prints the following output.

Order Detail: Roofing Nails
Order Detail: Circular Saw
Order Detail: Exterior Satin Paint
Order Detail: 20 Pieces Lumber

As you can see iterators are a convenient method to make loop through the standard collections or make your own types iterable.

Files

Rust has very capable built-in support for file handling like other languages. It provides a standard library std::fs for file operations. This library includes functions for opening, reading and writing files, and other file manipulation functions.

Let’s look first at a simple example of opening a file. For this to work it would need access to a file called myfile.txt of course.

use std::fs::File;

fn main() -> Result<(), std::io::Error> {
    let file = File::open("myfile.txt")?;
    print!("The file: {:?}",file);
    Ok(())
}

Notice main is defined to return a Result enum so it handles file errors without a panic. Also, notice the use for std::fs::File. This is as simple as file operations can get.

How about reading line by line. This is an example of this, notice how we still declare the main to return a Result in this case.

use std::fs::File;
use std::io::{BufRead, BufReader};

fn read_file(name: &str) -> Result<(), std::io::Error> {
    let file = File::open(name)?;
    let reader = BufReader::new(file);

    for line in reader.lines() {
        println!("{}", line?);
    }

    Ok(())
}

fn main() -> Result<(), std::io::Error> {
    read_file("myfile.txt")
}

The for loop works fine, but what you will often see is the use of read_line method on file.

This looks like this.

use std::fs::File;
use std::io::{BufRead, BufReader};

fn read_file(name: &str) -> Result<(), std::io::Error> {
    let file = File::open(name)?;
    let mut reader = BufReader::new(file);

    let mut line = String::new();
    loop {
        let bytes_read = reader.read_line(&mut line)?;
        if bytes_read == 0 {
            break;
        }
        println!("Line -> {}", line.trim());
        line.clear();
    }

    Ok(())
}

fn main() -> Result<(), std::io::Error> {
    read_file("myfile.txt")
}

This is the end of our file discussion, this will give you enough background to successfully work with files in Rust.

Summary

Again a lot of ground was covered. Macros should now be less of a mystery if you followed closely the explanation and examples. Iterators too, a useful and convenient way to loop through values as well as provide that functionality to your own structures. We did this for an Order/Details type example. Finally file handling should be familiar to you and provide a solid foundation for your code. Hope you enjoyed this portion of our Learning Rust series.

Enjoy the journey!

🔔 If you enjoyed this, subscribe to my future articles or view already published here. 🚀

📝 Have questions or suggestions? Leave a comment or message me through Medium.

Thank you for your support! 🌟

Rust
Iterators
File Handling
Programming
Metaprogramming
Recommended from ReadMedium