Using Match – Bob Revisited

We return to our Bob experiment and increase his version number twice!

TRY AGAIN in Scrabble letter - as I try again with an Exercism task, this time using match.
Wait, “TG”? That’s not a word! Challenge!

Remember Bob from a previous post – our tiniest of AI-responders? I got feedback from the Exercism tutor that I had avoided one of Rust‘s most versatile of operators and I should think about using match. So, not to be a jerk, but just to see if I could replace ALL of the ifs in my method, I quickly came up with this solution.

Switch to Using Match

pub fn reply(message: &str) -> &str {
    let message = message.trim();
    let question = message.ends_with('?');
    let any_alpha = message.chars().any(char::is_alphabetic);
    let is_yelling = message.to_ascii_uppercase() == message;
    let is_empty = message.len() == 0;

    match message.is_empty() {
        true => "Fine. Be that way!",
        _ => match any_alpha && is_yelling {
            true => match question {
                true => "Calm down, I know what I'm doing!",
                _ => "Whoa, chill out!",
            },
            _ => match !is_empty && question {
                true => "Sure.",
                _ => "Whatever.",
            },
        },
    }
}

Come On Bob, Get With the Rust

So, it may be my newness to Rust, but I’m not sure version 2 is “more maintainable” code than what I started with. I had thought this Exercism task was just about learning some string methods. Turns out, it is also about learning the match operator. Of course, you wouldn’t code any parser or AI this way anyway – this IS just for practice.

Another tiny thing the tutor pointed out, I did have let check_message = message.trim() in my earlier code. A more idiomatic way in Rust is to reassign it to the same variable, hence the let message = message.trim() in this version. My guess is: less memory allocation, fewer variables for you to keep track of, and you aren’t able to incorrectly use the wrong variable later in the method. Actually, that probably isn’t a Rust idea – that’s just a good programming tip.




Bob With a Trait

Patterns are, I’m told, very powerful in Rust and that’s a big reason to start using match wherever you can. I’ve also learned a bit about traits and impls which I talked about in an earlier post. And that got me thinking maybe I’d work through the syntax to get it working with a trait instead. That idea led to this code.

pub fn reply(message: &str) -> &str {
    enum Quality {
        Empty,
        YellQuestion,
        YellStatement,
        AskQuestion,
        Statement,
    };

    trait HasQuality {
        fn has_quality(&self) -> Quality;
    }

    impl HasQuality for str {
        fn has_quality(&self) -> Quality {
            let message = self.trim();
            let question = message.ends_with('?');
            let any_alpha = message.chars().any(char::is_alphabetic);
            let is_yelling = message.to_ascii_uppercase() == message;
            let is_empty = message.len() == 0;

            match message.is_empty() {
                true => Quality::Empty,
                _ => match any_alpha && is_yelling {
                    true => match question {
                        true => Quality::YellQuestion,
                        _ => Quality::YellStatement,
                    },
                    _ => match !is_empty && question {
                        true => Quality::AskQuestion,
                        _ => Quality::Statement,
                    },
                },
            }
        }
    };

    match message.has_quality() {
        Quality::Empty => "Fine. Be that way!",
        Quality::YellQuestion => "Calm down, I know what I'm doing!",
        Quality::YellStatement => "Whoa, chill out!",
        Quality::AskQuestion => "Sure.",
        _ => "Whatever.",
    }
}

So here, we come up with the idea of a Quality trait and then we implement that trait for the built-in str primative. We’ve expanded what you can do to a str with our own trait! Version 3 of Bob really helps reveal some of the Rust thought patterns I need to hone.

Of course, I made sure Version 3 still passes all of the Exercism tests for this task. This change was approaching my limit of Rust knowledge to get it working without help from a book – just obeying every compiler complaint. However, I cranked this out much faster than previous Rust code, so I think some of this learning (and blogging) is sinking in! I surprised myself enough, that I posted this solution to Exercism as well. I want to hear what the tutor has to say about this method (no pun intended!). Now, I just need to remember to keep using match, traits, impls, and other Rust-supplied power!

Update: 2019-07-17 13:30

Woah, I just got some great advice from the Exercism tutor! You can match on a (expression, expression, ...) so check this out!

    impl HasQuality for str {
        fn has_quality(&self) -> Quality {
            let message = self.trim();
            let question = message.ends_with('?');
            let any_alpha = message.chars().any(char::is_alphabetic);
            let is_yelling = message.to_ascii_uppercase() == message;
            let is_empty = message.len() == 0;

            match (is_empty, any_alpha && is_yelling, question) {
                (true, _, _) => Quality::Empty,
                (false, true, true) => Quality::YellQuestion,
                (false, true, false) => Quality::YellStatement,
                (false, false, true) => Quality::AskQuestion,
                _ => Quality::Statement,
            }
        }
    };

Return to String Basics

Where I try to give some life to poor “Bob”, but learn more about Clippy and Fmt

Pale robot face, is this the face of Bob?
I’m sorry “Bob”, I cannot do that.

Another sample programming problem and I am about to practice so string methods, but also learn about two Rust helpers: Cargo Clippy and Fmt. It is becoming apparent just how much help in Rust coding is embedded in the error messages and in cargo.

The next Exercism programming task was to have “Bob” respond in various ways to a passed in prompt. If you ask a question, Bob should respond with “Sure.” unless you are SCREAMING, in which case his response is “Calm down, I know what I’m doing!”. If you prompt “Bob” with nothing, he says “Fine. Be that way!”, but everything gets back a simple “Whatever.”

Coding Like a Rust Coder

Of course, there are many ways to check all these conditions. Still, I wanted to use the built-in helper methods and find the most Rust-idiomatic way. I was also cognizant of not repeating myself: not running the same method or checking the same test more than once and wasting clock cycles.

For instance, string.trim() removes leading and trailing whitespace, and combining that with a built-in test, we can see if the prompt is basically empty by using string.trim().is_empty()

I choose to see if “Bob” is being yelled at, but uppercasing the string and seeing if nothing changed – of course, that’s only true if the string includes at least 1 alphabetic character, so lets check that first with string.chars().any(char::is_alphabetic).

That will iterate using the chars() that we’ve seen before and tell me if any of them are a-z/A-Z. To uppercase the string (I’m only going to worry about the ASCII characters here; Rust also has to_uppercase which will follow the terms of the Unicode Derived Core Property Uppercase, see this for some issues with Unicode characters), so I’ll just use string.to_ascii_uppercase().




Searching for Possibilities

Lastly, it might be a “whispered question”, but I want to make sure the prompt isn’t entirely just a question mark – that doesn’t fit the premise of a question according to the Exercism-provided test I am still failing, so string.len() will give me the length of the string. I end up with this code:

 pub fn reply(message: &str) -> &str {
    let check_message = message.trim();

    if check_message.is_empty() {
        return "Fine. Be that way!";
    }

    let question = check_message.ends_with('?');

    if check_message.chars().any(char::is_alphabetic)
        && check_message.to_ascii_uppercase() == message
    {
        if question {
            return "Calm down, I know what I'm doing!";
        } else {
            return "Whoa, chill out!";
        }
    }

    if check_message.len() > 1 && question {
        return "Sure.";
    }
    "Whatever."
} 

So, “not running the same method or checking the same test more than once” meant I had to make a copy of the passed in string so I could trim() it just once at the beginning. I also didn’t want to test for a ‘?’ at the end of the prompt more than one time. Maybe my extra variable creation costs more than running trim() each time I would need it, I’m not sure!

Cargo Clippy and Fmt

Bad sci-fi movie robot, maybe his name can be Cargo Clippy
Cargo Clippy is here to help!

Another note: I used two Rust assistants: first, clippy to check my code before submitting it to Exercism. Clippy is basically a Rust lint program and will alert you to changes you can make to your code even though it compiles. For instance, I learned about is_empty() (instead of checking len() == 0) and I learned that ends_with() can apparently take a char OR a string (I originally had ends_with(“?”) since I was calling a method on a String type and I just assumed…)

You can get “clippy with it” (no, he didn’t!) by typing rustup component add clippy. I then ran it against my code by doing cargo clippy –all-targets.

Also, there is a prettifier called fmt; get it by doing rustup component add fmt and then run cargo fmt. This “fixed” my indentation and curly braces (and probably other things) to match what is commonly accepted correct-formatting for Rust scripts.

1 Mississippi, 2 Mississippi…

Where I learn about crates and modules and time… and sheep

Recycle logo on bag
Re-use code to make their owners proud!

Let’s learn about stealing reusing code! Rust has a developing library of code, from contributors, that you pull into your own programs and then not have to reinvent the wheel. Perl calls these modules and has CPAN as a repository and for searches, updates, and documentation. In Rust, these are instead called library crates and you can look around at the crates.io repository and others – plus create your own, internal repositories.

Searching Through Crates

For instance, go to crates.io and search for “chrono“. You should come to the chrono crates.io page. Right now, I’m seeing the most recent version as 0.4.7. Note if you had searched for “date time” instead, you’ll find many results, and the one with the huge number of downloads is chrono!

Like CPAN, crates.io can tell you the latest version (and how recent it is), if the current build of the crate is passing tests, usage information, and more. Note that if you click on the documentation link on the chrono crate page, you are taken to the docs.rs page: docs.rs/chrono/0.4.6/chrono/ and can read the full documentstion.

Now, we come to the term modules as it applies to Rust. A Rust crate has a default module, but can also have additional modules, providing additional features. For instance, from the docs.rs page about chrono, click modules on the left-side and you will see which modules are a part of chrono: format, naive, offset, prelude, and serde. We won’t need any of those additional modules, just some pieces of what the default chrono crate provides.

Rust Chrono Crate

Sheep and lamb looking at us
One second for every sheep in the world – plus some sheep left over.

I see our next Exercism problem uses the chrono crate in the solution (coincidence?!) Let’s take a look! So, they want us to display the date and time when 1 Gigasecond, from a given date and time, would occur. That is, given a date and time, figure out the date and time 1,000,000,000 seconds later. Who needs this? I dunno, but who cares about that.

Looking in the tests/gigasecond.rs file from Exercism, where the tests for this package are stored, the first one will be giving our code Utc.ymd(2011, 4, 25).and_hms(0, 0, 0) and expecting back Utc.ymd(2043, 1, 1).and_hms(1, 46, 40). Looks like a gigasecond is just shy of 32 years. If we check tests by running cargo test, we can see, of course, that it fails – we haven’t done any work yet!




Crate Semantic Versioning

Exercism did help us out though – they had already figured out the most popular crate to use for date/time types and added it as a dependency to our project. Check out the included Cargo.toml file and notice the one dependency at the end: chrono = “0.4”. Cargo looks on crates.io by default, and the version number listed is a semver (for semantic versioning). It is like a pattern to be used to determine which versions are allowed to be used for this project. In this case, specifying 0.4 means the latest 0.4.x version of the library should be pulled (so it will pull the 0.4.7 version we saw as the latest), but Cargo will not advance to 0.5. You’d have to test 0.5 and, if your code still works, modify your toml file manually. This prevents a potentially major upgrade of a library to suddenly break your code in your production deployment cycle.

Geese
We don’t tweet, we jabber – but not about Rust!

Enough jabbering – with all of the help (and documentation) chrono gives us, and all that background knowledge, I turn this into a single expression like prior problems we’ve had:

 use chrono::{DateTime, Utc, Duration}; 

 pub fn after(start: DateTime) -> DateTime {
     start + Duration::seconds(1_000_000_000)
 }

Exercism gave us use chrono::{DateTime, Utc}; in the answer already, I just tacked on “, Duration” when I realized I needed that struct of the chrono crate as well. We are passed in the parameter start, we add 1,000,000,000 seconds as a Duration type. Then the expression calculates and returns a DateTime result.

We could have written this without the use statement but I hope you’ll see its help in shortening the code is worth it:

 pub fn after(start: chrono::DateTime)
 -> chrono::DateTime {
     start + chrono::Duration::seconds(1_000_000_000)
 }

Light’s Green, Trap is Clean

Anyway, running cargo test now is green! We also run cargo test — –ignored to run all of the additional tests the Exercism team wrote, and they all pass as well!

So, another simple answer – but ONLY because someone went to all the trouble of DateTime and Duration processing and manipulation!