6
\$\begingroup\$

In order to learn Rust I'm going through The Rust Book and did the implemented a program for the following exercise:

Print the lyrics to the Christmas carol “The Twelve Days of Christmas,” taking advantage of the repetition in the song.

I have decided to use lookup tables for the repeating lines of the song and ordinals because I believe those to be good for performance.

I'd appreciate any comments on my code, especially any suggestions on how to avoid the recurring magic number 12.

type Day = u8;

const LYRICS: [&str; 12] = [
    "a partridge in a pear tree",
    "two turtle doves",
    "three French hens",
    "four calling birds",
    "five gold rings",
    "six geese a laying",
    "seven swans a swimming",
    "eight maids a milking",
    "nine ladies dancing",
    "ten lords a leaping",
    "eleven pipers piping",
    "twelve drummers drumming",
];

const ORDINALS: [&str; 12] = [
        "first",
        "second",
        "third",
        "fourth",
        "fifth",
        "sixth",
        "seventh",
        "eigth",
        "ninth",
        "tenth",
        "eleventh",
        "twelfth",
];

fn main() {
    for day in 0..12 {
        if day != 0 {
            println!();
        }

        print_first_line(day);
        print_lyrics(day);
    }
}

fn print_first_line(day: Day) {
    println!(
        "On the {} day of Christmas my true love gave to me",
        ORDINALS[day as usize],
    )
}

fn print_lyrics(day: Day) {
    for line in (0..=day).rev() {
        if line == 0 && day != 0 {
            println!("And {}", LYRICS[line as usize]);
        } else {
            println!("{}", capitalize(LYRICS[line as usize]));
        }
    }
}

fn capitalize(text: &str) -> String {
    let mut chars = text.chars();

    let mut new_text = String::with_capacity(text.len());

    if let Some(c) = chars.next() {
        new_text.extend(c.to_uppercase());
        new_text.extend(chars);
    }

    new_text
}
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

Nicely done, I have very little negative to say! Most of this answer will be an exploration into some more advanced ways of doing what you are doing, overkill for this case, but very useful in larger projects.

  • The day as usize everywhere is a bit of a distraction. I definitely understand the desire to store a small int when your data is known to be small (i.e. at most 11), but in this case you are only using Day as an index, so using type Day = usize directly seems like better design.

  • However, if you want to be fancy (and/or learn a bit about implementing your own types in Rust), you have an alternative which I will describe below: the basic idea is to implement your own types (use struct A(B) instead of type A = B), then implement the Index trait to index using Day directly. For a small piece of code like this, it doesn't make a huge difference, but for large projects it's really helpful to avoid accidental mistakes like indexing an array with the wrong variable (i.e. in your case, indexing with some random usize in context instead of a Day between 0 and 11). If you do that you get a type error, and this also allows you to abstract better: treating Day as an opaque type that is used to get the lyrics from your lyrics objects, rather than as an integer with arbitrary functionality. As an added bonus, this also helps avoid the magic number 12 problem that you mention.

    Diving in, we start by defining new types (struct A(B)) for Day and for a list indexed by Day. The #[derive(...)] is telling Rust that we want Days to be able to be copied, checked for equality, and printed out, copying over that functionality from u8.

    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    struct Day(u8);
    
    #[derive(Debug)]
    struct DayList<T>([T; 12]);
    
    const FIRST_DAY: Day = Day(0);
    const LAST_DAY: Day = Day(11);
    
    const LYRICS: DayList<&str> = DayList([
        ...
    

    Then, we implement the ability to index DayList objects with Day. For a variable of type A where we defined struct A(B), we use x.0 to get the underlying B data. (This explicitness is part of the point of doing this, we make sure to keep A variables and B variables separate.)

    impl<T> Index<Day> for DayList<T> {
        type Output = T;
        fn index(&self, day: Day) -> &T {
            self.0.index(day.0 as usize)
        }
    }
    

    Finally, we want to be able to iterate over days, something like this utility function seems most useful for your code:

    fn days_upto(day: Day) -> impl DoubleEndedIterator<Item = Day> {
        (0..=day.0).map(Day)
    }
    

    This takes a Day and iterates over all days up to and including it. Perhaps this seems like overkill! But we get a lot of benefit out of it: now we can modify the rest of your code to not have indices at all, and only iterate over Days directly and index DayList with Day. Type safety! Here's how it looks:

    fn main() {
        for day in days_upto(LAST_DAY) {
            if day != FIRST_DAY {
                println!();
            }
    
            print_first_line(day);
            print_lyrics(day);
        }
    }
    
    fn print_first_line(day: Day) {
        println!(
            "On the {} day of Christmas my true love gave to me",
            ORDINALS[day],
        )
    }
    
    fn print_lyrics(day: Day) {
        for line in days_upto(day).rev() {
            if line == FIRST_DAY && day != FIRST_DAY {
                println!("And {}", LYRICS[line]);
            } else {
                println!("{}", capitalize(LYRICS[line]));
            }
        }
    }
    
  • Finally: the if block

    if day != FIRST_DAY {
        println!();
    }
    

    can be avoided if you print a title with the lyrics as well, which seems realistic. E.g.:

    println!("The Twelve Days of Christmas");
    println!("----------------------------");
    for day in days_upto(LAST_DAY) {
        println!();
        print_first_line(day);
        print_lyrics(day);
    }
    
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.