Macro Fizzbuzz

This video came up in my Youtube recommendations: Is My Fizzbuzz Solution Terrible?, which is a follow-up to their previous video One Simple Trick to Instantly Improve Your Code - Finishing Tom Scott’s FizzBuzz, which itself is a response to Tom Scott’s video FizzBuzz: One Simple Interview Question.

Naturally, that was my instinctive response, so I whipped up something quick.

use std::io:: as _;

macro_rules!  {
    // 1. The ,* $(,)? incantation at the end allows trailing commas
    ($range:expr, $(($divisor:literal, $replacement:literal)),* $(,)?) => {
        // 2a. Create a new block so the stdout guard is dropped after the loop
        {
            // 2b. Lock standard output once to prevent frequent relocking
            let mut stdout = std::io::stdout().lock();

            // 3a. Reuse underlying string allocation
            let mut s = String::new();
            for i in $range {
                // Macro overloading in action, calling the second macro arm
                $(fizzbuzz!(s, i, $divisor, $replacement);)+

                if s.is_empty() {
                    writeln!(stdout, "{i}")?;
                } else {
                    writeln!(stdout, "{s}")?;
                }

                // 3b. Clear string contents while preserving underlying buffer
                s.clear();
            }
        }
    };

    // The actual remainder comparison
    ($s:ident, $i:ident, $divisor:literal, $replacement:literal) => {
        if $i % $divisor == 0 {
            $s.push_str($replacement);
        }
    };
}

fn () -> <(), <dyn std::error::>> {
    !(
        1..101,
        (3, "Fizz"),
        (5, "Buzz"),
        (7, "Bazz"),
        (11, "Bizz"),
        (13, "Biff")
    );

    (())
}

Using cargo-expand, we can examine the expansion of the macro:

#![(prelude_import)]
#[]
use std::prelude::::*;
#[]
extern crate std;
use std::io:: as _;
fn () -> <(), <dyn std::error::>> {
    {
        let mut  = std::io::().();
        let mut  = ::();
        for  in 1..101 {
            if  % 3 == 0 {
                .("Fizz");
            }
            if  % 5 == 0 {
                .("Buzz");
            }
            if  % 7 == 0 {
                .("Bazz");
            }
            if  % 11 == 0 {
                .("Bizz");
            }
            if  % 13 == 0 {
                .("Biff");
            }
            if .() {
                .(!("{0}\n", ))?;
            } else {
                .(!("{0}\n", ))?;
            }
            .();
        }
    };
    (())
}

…which is identical to the solution showed by Tom Scott at the end of his video but without the explicit code duplication.

I’ll leave implementing Fizzbuzz using macros in a different language as an exercise for the reader.

std::io
pub trait Write

A trait for objects which are byte-oriented sinks.

Implementors of the Write trait are sometimes called ‘writers’.

Writers are defined by two required methods, [write] and [flush]:

  • The [write] method will attempt to write some data into the object, returning how many bytes were successfully written.

  • The [flush] method is useful for adapters and explicit buffers themselves for ensuring that all buffered data has been pushed out to the ‘true sink’.

Writers are intended to be composable with one another. Many implementors throughout [std::io] take and provide types which implement the Write trait.

Examples

use std::io::prelude::*;
use std::fs::File;

fn main() -> std::io::Result<()> {
    let data = b"some bytes";

    let mut pos = 0;
    let mut buffer = File::create("foo.txt")?;

    while pos < data.len() {
        let bytes_written = buffer.write(&data[pos..])?;
        pos += bytes_written;
    }
    Ok(())
}

The trait also provides convenience methods like [write_all], which calls write in a loop until its entire input has been written.

alloc::boxed
pub struct Box<T, A = Global>(Unique<T>, A)
where
    T: ?Sized,
    A: Allocator,

A pointer type that uniquely owns a heap allocation of type T.

See the module-level documentation for more.

let mut stdout: StdoutLock<'static>
#[macro_use]

Valid forms are:

  • #[macro_use]
  • #[macro_use(name1, name2, …)]
core::result
pub enum Result<T, E> {
    Ok( /* … */ ),
    Err( /* … */ ),
}

Result is a type that represents either success (Ok) or failure (Err).

See the documentation for details.

alloc::string::String
pub const fn is_empty(&self) -> bool

Returns true if this String has a length of zero, and false otherwise.

Examples

let mut v = String::new();
assert!(v.is_empty());

v.push('a');
assert!(!v.is_empty());
#[prelude_import]

Valid forms are:

  • #[prelude_import]
alloc::string
pub struct String {
    vec: Vec<u8>,
}

A UTF-8–encoded, growable string.

String is the most common string type. It has ownership over the contents of the string, stored in a heap-allocated buffer (see Representation). It is closely related to its borrowed counterpart, the primitive [str].

Examples

You can create a String from a literal string with [String::from]:

let hello = String::from("Hello, world!");

You can append a char to a String with the [push] method, and append a [&str] with the [push_str] method:

let mut hello = String::from("Hello, ");

hello.push('w');
hello.push_str("orld!");

If you have a vector of UTF-8 bytes, you can create a String from it with the [from_utf8] method:

// some bytes, in a vector
let sparkle_heart = vec![240, 159, 146, 150];

// We know these bytes are valid, so we'll use `unwrap()`.
let sparkle_heart = String::from_utf8(sparkle_heart).unwrap();

assert_eq!("💖", sparkle_heart);

UTF-8

Strings are always valid UTF-8. If you need a non-UTF-8 string, consider OsString. It is similar, but without the UTF-8 constraint. Because UTF-8 is a variable width encoding, Strings are typically smaller than an array of the same chars:

use std::mem;

// `s` is ASCII which represents each `char` as one byte
let s = "hello";
assert_eq!(s.len(), 5);

// A `char` array with the same contents would be longer because
// every `char` is four bytes
let s = ['h', 'e', 'l', 'l', 'o'];
let size: usize = s.into_iter().map(|c| mem::size_of_val(&c)).sum();
assert_eq!(size, 20);

// However, for non-ASCII strings, the difference will be smaller
// and sometimes they are the same
let s = "💖💖💖💖💖";
assert_eq!(s.len(), 20);

let s = ['💖', '💖', '💖', '💖', '💖'];
let size: usize = s.into_iter().map(|c| mem::size_of_val(&c)).sum();
assert_eq!(size, 20);

This raises interesting questions as to how s[i] should work. What should i be here? Several options include byte indices and char indices but, because of UTF-8 encoding, only byte indices would provide constant time indexing. Getting the ith char, for example, is available using [chars]:

let s = "hello";
let third_character = s.chars().nth(2);
assert_eq!(third_character, Some('l'));

let s = "💖💖💖💖💖";
let third_character = s.chars().nth(2);
assert_eq!(third_character, Some('💖'));

Next, what should s[i] return? Because indexing returns a reference to underlying data it could be &u8, &[u8], or something else similar. Since we’re only providing one index, &u8 makes the most sense but that might not be what the user expects and can be explicitly achieved with [as_bytes()]:

// The first byte is 104 - the byte value of `'h'`
let s = "hello";
assert_eq!(s.as_bytes()[0], 104);
// or
assert_eq!(s.as_bytes()[0], b'h');

// The first byte is 240 which isn't obviously useful
let s = "💖💖💖💖💖";
assert_eq!(s.as_bytes()[0], 240);

Due to these ambiguities/restrictions, indexing with a usize is simply forbidden:

let s = "hello";

// The following will not compile!
println!("The first letter of s is {}", s[0]);

It is more clear, however, how &s[i..j] should work (that is, indexing with a range). It should accept byte indices (to be constant-time) and return a &str which is UTF-8 encoded. This is also called “string slicing”. Note this will panic if the byte indices provided are not character boundaries - see [is_char_boundary] for more details. See the implementations for [SliceIndex<str>] for more details on string slicing. For a non-panicking version of string slicing, see [get].

The [bytes] and [chars] methods return iterators over the bytes and codepoints of the string, respectively. To iterate over codepoints along with byte indices, use [char_indices].

Deref

String implements [Deref]<Target = [str]>, and so inherits all of [str]’s methods. In addition, this means that you can pass a String to a function which takes a [&str] by using an ampersand (&):

fn takes_str(s: &str) { }

let s = String::from("Hello");

takes_str(&s);

This will create a [&str] from the String and pass it in. This conversion is very inexpensive, and so generally, functions will accept [&str]s as arguments unless they need a String for some specific reason.

In certain cases Rust doesn’t have enough information to make this conversion, known as [Deref] coercion. In the following example a string slice &'a str implements the trait TraitExample, and the function example_func takes anything that implements the trait. In this case Rust would need to make two implicit conversions, which Rust doesn’t have the means to do. For that reason, the following example will not compile.

trait TraitExample {}

impl<'a> TraitExample for &'a str {}

fn example_func<A: TraitExample>(example_arg: A) {}

let example_string = String::from("example_string");
example_func(&example_string);

There are two options that would work instead. The first would be to change the line example_func(&example_string); to example_func(example_string.as_str());, using the method [as_str] to explicitly extract the string slice containing the string. The second way changes example_func(&example_string); to example_func(&*example_string);. In this case we are dereferencing a String to a [str], then referencing the [str] back to [&str]. The second way is more idiomatic, however both work to do the conversion explicitly rather than relying on the implicit conversion.

Representation

A String is made up of three components: a pointer to some bytes, a length, and a capacity. The pointer points to the internal buffer which String uses to store its data. The length is the number of bytes currently stored in the buffer, and the capacity is the size of the buffer in bytes. As such, the length will always be less than or equal to the capacity.

This buffer is always stored on the heap.

You can look at these with the [as_ptr], [len], and [capacity] methods:

use std::mem;

let story = String::from("Once upon a time...");

// Prevent automatically dropping the String's data
let mut story = mem::ManuallyDrop::new(story);

let ptr = story.as_mut_ptr();
let len = story.len();
let capacity = story.capacity();

// story has nineteen bytes
assert_eq!(19, len);

// We can re-build a String out of ptr, len, and capacity. This is all
// unsafe because we are responsible for making sure the components are
// valid:
let s = unsafe { String::from_raw_parts(ptr, len, capacity) } ;

assert_eq!(String::from("Once upon a time..."), s);

If a String has enough capacity, adding elements to it will not re-allocate. For example, consider this program:

let mut s = String::new();

println!("{}", s.capacity());

for _ in 0..5 {
    s.push_str("hello");
    println!("{}", s.capacity());
}

This will output the following:

0
8
16
16
32
32

At first, we have no memory allocated at all, but as we append to the string, it increases its capacity appropriately. If we instead use the [with_capacity] method to allocate the correct capacity initially:

let mut s = String::with_capacity(25);

println!("{}", s.capacity());

for _ in 0..5 {
    s.push_str("hello");
    println!("{}", s.capacity());
}

We end up with a different output:

25
25
25
25
25
25

Here, there’s no need to allocate more memory inside the loop.

core::error
pub trait Error
where
    Self: Debug + Display,

Error is a trait representing the basic expectations for error values, i.e., values of type E in Result<T, E>.

Errors must describe themselves through the Display and Debug traits. Error messages are typically concise lowercase sentences without trailing punctuation:

let err = "NaN".parse::<u32>().unwrap_err();
assert_eq!(err.to_string(), "invalid digit found in string");

Errors may provide cause information. Error::source is generally used when errors cross “abstraction boundaries”. If one module must report an error that is caused by an error from a lower-level module, it can allow accessing that error via Error::source. This makes it possible for the high-level module to provide its own errors while also revealing some of the implementation for debugging.

core::result::Result
Ok(T)

Contains the success value

std::io::stdio
pub fn stdout() -> Stdout

Constructs a new handle to the standard output of the current process.

Each handle returned is a reference to a shared global buffer whose access is synchronized via a mutex. If you need more explicit control over locking, see the Stdout::lock method.

Note: Windows Portability Considerations

When operating in a console, the Windows implementation of this stream does not support non-UTF-8 byte sequences. Attempting to write bytes that are not valid UTF-8 will return an error.

In a process with a detached console, such as one using #![windows_subsystem = "windows"], or in a child process spawned from such a process, the contained handle will be null. In such cases, the standard library’s Read and Write will do nothing and silently succeed. All other I/O operations, via the standard library or via raw Windows API calls, will fail.

Examples

Using implicit synchronization:

use std::io::{self, Write};

fn main() -> io::Result<()> {
    io::stdout().write_all(b"hello world")?;

    Ok(())
}

Using explicit synchronization:

use std::io::{self, Write};

fn main() -> io::Result<()> {
    let stdout = io::stdout();
    let mut handle = stdout.lock();

    handle.write_all(b"hello world")?;

    Ok(())
}
alloc::string::String
pub const fn new() -> String

Creates a new empty String.

Given that the String is empty, this will not allocate any initial buffer. While that means that this initial operation is very inexpensive, it may cause excessive allocation later when you add data. If you have an idea of how much data the String will hold, consider the [with_capacity] method to prevent excessive re-allocation.

Examples

let s = String::new();
core::macros::builtin
macro_rules! format_args

Constructs parameters for the other string-formatting macros.

This macro functions by taking a formatting string literal containing {} for each additional argument passed. format_args! prepares the additional parameters to ensure the output can be interpreted as a string and canonicalizes the arguments into a single type. Any value that implements the [Display] trait can be passed to format_args!, as can any [Debug] implementation be passed to a {:?} within the formatting string.

This macro produces a value of type [fmt::Arguments]. This value can be passed to the macros within std::fmt for performing useful redirection. All other formatting macros (format!, write, println!, etc) are proxied through this one. format_args!, unlike its derived macros, avoids heap allocations.

You can use the [fmt::Arguments] value that format_args! returns in Debug and Display contexts as seen below. The example also shows that Debug and Display format to the same thing: the interpolated format string in format_args!.

let debug = format!("{:?}", format_args!("{} foo {:?}", 1, 2));
let display = format!("{}", format_args!("{} foo {:?}", 1, 2));
assert_eq!("1 foo 2", display);
assert_eq!(display, debug);

See the formatting documentation in std::fmt for details of the macro argument syntax, and further information.

Examples

use std::fmt;

let s = fmt::format(format_args!("hello {}", "world"));
assert_eq!(s, format!("hello {}", "world"));

Lifetime limitation

Except when no formatting arguments are used, the produced fmt::Arguments value borrows temporary values, which means it can only be used within the same expression and cannot be stored for later use. This is a known limitation, see #92698.

#[feature]

Valid forms are:

  • #[feature(name1, name2, …)]
src
fn main() -> Result<(), Box<dyn std::error::Error>>
let mut s: String
src
macro_rules! fizzbuzz
std::io::stdio::Stdout
pub fn lock(&self) -> StdoutLock<'static>

Locks this handle to the standard output stream, returning a writable guard.

The lock is released when the returned lock goes out of scope. The returned guard also implements the Write trait for writing data.

Examples

use std::io::{self, Write};

fn main() -> io::Result<()> {
    let mut stdout = io::stdout().lock();

    stdout.write_all(b"hello world")?;

    Ok(())
}
alloc::string::String
pub fn push_str(&mut self, string: &str)

Appends a given string slice onto the end of this String.

Examples

let mut s = String::from("foo");

s.push_str("bar");

assert_eq!("foobar", s);
alloc::string::String
pub fn clear(&mut self)

Truncates this String, removing all contents.

While this means the String will have a length of zero, it does not touch its capacity.

Examples

let mut s = String::from("foo");

s.clear();

assert!(s.is_empty());
assert_eq!(0, s.len());
assert_eq!(3, s.capacity());
std::prelude
pub mod rust_2021

The 2021 version of the prelude of The Rust Standard Library.

See the module-level documentation for more.

std::io::Write
pub trait Write
pub fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) -> Result<()>

Writes a formatted string into this writer, returning any error encountered.

This method is primarily used to interface with the format_args!() macro, and it is rare that this should explicitly be called. The write!() macro should be favored to invoke this method instead.

This function internally uses the [write_all] method on this trait and hence will continuously write data so long as no errors are received. This also means that partial writes are not indicated in this signature.

Errors

This function will return any I/O error reported while formatting.

Examples

use std::io::prelude::*;
use std::fs::File;

fn main() -> std::io::Result<()> {
    let mut buffer = File::create("foo.txt")?;

    // this call
    write!(buffer, "{:.*}", 2, 1.234567)?;
    // turns into this:
    buffer.write_fmt(format_args!("{:.*}", 2, 1.234567))?;
    Ok(())
}
let i: i32