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,
            }
        }
    };

Config Solutions

Embossed compass legend

So, because match gave me some problems (hrm, what Rust troubles are yet to come!?), this took me a minute to get right. These changes allow us to configure the encoding characters to be defined in .json config files. Plus, a platform level (development, stage, production) can be set and configured values can be overridden via environment variables.

Code Dump Coming…

extern crate config;
extern crate serde;

#[macro_use]
extern crate serde_derive;

mod settings;

use settings::Settings;

fn main() {
    for arg in std::env::args().skip(1) {
      let encoded = encode(&arg);
      let decoded = decode(&encoded);
      assert_eq!(arg, decoded);

      println!("'{}' encodes as '{}' and decode works",
          &arg, &encoded);
    }
 }

fn encode(orig: &str) -> String {
    let settings = Settings::new();

    let mut advance = '1';
    let mut print = '0';
    match settings {
        Ok(s) => {
            advance = s.advance;
            print = s.print;
        },
        Err(_) => {}
    }

    let mut str = String::new();
    let mut cur: u8 = 0;

    for c in orig.chars() {
      while c != cur as char {
        cur = cur.wrapping_add(1);
        str.push(advance);
      }
      str.push(print);
    }
    str
}

fn decode(code: &str) -> String {
    let settings = Settings::new();

    let mut advance = '1';
    let mut print = '0';
    match settings {
        Ok(s) => {
            advance = s.advance;
            print = s.print;
        },
        Err(_) => {}
    }
    let mut str = String::new();
    let mut cur: u8 = 0;

    for c in code.chars() {
        match c {
            a if a == advance => cur = cur.wrapping_add(1),
            b if b == print => str.push(cur as char),
            _ => {},
        }
    }
    str
}



And now I have settings.rs as well:

use std::env;
use config::{ConfigError, Config, File, Environment};

#[derive(Debug, Deserialize)]
pub struct Settings {
    pub debug: bool,
    pub advance: char,
    pub print: char,
}

impl Settings {
    pub fn new() -> Result<Self, ConfigError> {
        let mut s = Config::new();

        // Start off by merging in the "default" configuration file
        s.merge(File::with_name("conf/default"))?;

        // Add in the current environment file
        // Default to 'development' env
        // Note that this file is _optional_
        let env = env::var("RUN_MODE").unwrap_or("development".into());
        s.merge(File::with_name(&format!("conf/{}", env)).required(false))?;

        // Add in a local configuration file
        // This file shouldn't be checked in to git
        s.merge(File::with_name("conf/local").required(false))?;

        // Add in settings from the environment (with a prefix of APP)
        // Eg.. `APP_DEBUG=1 ./target/app` would set the `debug` key
        s.merge(Environment::with_prefix("app"))?;

        // You may also programmatically change settings
        //s.set("database.url", "postgres://")?;

        // Now that we're done, let's access our configuration
        //println!("debug: {:?}", s.get_bool("debug"));
        //println!("database: {:?}", s.get::<String>("database.url"));

        // You can deserialize (and thus freeze) the entire configuration as
        s.try_into()
    }
}

and some config files too!

conf/default.json:

{
    "debug": 1
}

conf/development.json:

 {
    "advance": "#",
    "print": "@"
}

Can I Talk My Way Through That?

Feel free to comment about what I did wrong, because at the very least, my settings processing seems wrong and repetitive! As bad as globals are, is that possible in Rust and better than passing it around or certainly better than having to go assign settings in every method that needs it?

Also, I understand making the settings struct be pub but why did I have to make each of its properties pub as well – that doesn’t seem right.

It took me a while to fully realize why match was called match and not switch! Because it is based on patterns and not straight conditionals! But these aren’t perl regex patterns, so don’t get excited! Even though they aren’t actual regex’s, they ARE “patterns”, so matching one variable against another isn’t straightforward like with switch/case.

    match c {
        a if a == advance => cur = cur.wrapping_add(1),
        b if b == print => str.push(cur as char),
        _ => {},
    }

So, to check if character c matches variables advance or print, we need (I think) to do it this way: a and b in each of those match arms serve as temporary variables to make the comparison and are lost as soon as the match statement ends. I’ll come back when I figure out the correct way, but hopefully someone will let me know!

Crazy (En) code and decode

Where I fiddle with a crazy encoding scheme

laptop in dark room with green code on screen
The 0s and 1s have me…

Encoding with Rust code – sounds fun! Pulling from a different “programming exercise” site this time: CodeWars. One of the very first exercises there is a crazy encoding which looks like:

++++++++++++++++++++++++++++
++++++++++++++++++++++++++++
++++++++++++++++.++++++++++++
+++++++++++++++++.+++++++..+++.

Starting at ASCII value 0 when we begin decoding, we start processing the string:
+ means we add 1 to the ASCII value we are holding
. means we print the character based on the current ASCII value

If you add 1 to get 256, that would loop back to 0.

So, that mess above equates to the string “Hello” (you can probably find the two ‘l’s in the encoded string).

So, lets whip up a function to decode that:

Coding Time

fn decode(code: &str) -> String {
    let mut str = String::new();
    let mut cur: u8 = 0;

    for c in code.chars() {
        match c {
            '+' => cur = cur.wrapping_add(1),
            '.' => str.push(cur as char),
            _ => {},
        }
    }
    str
}

Simple enough, we get an encoded string as a parameter, we start our cur variable at 0 and loop thru the characters of the string. A simple match lets us choose what to do. BTW, wrapping_add means we don’t have to check for 256 ourselves, it will wrap back to 0 automatically. Anything other than a + or . we simply ignore.

So, for fun, lets write an encode function as well:

fn encode(orig: &str) -> String {
    let mut str = String::new();
    let mut cur: u8 = 0;

    for c in orig.chars() {
        while c != cur as char {
            cur = cur.wrapping_add(1);
            str.push('+');
        }
        str.push('.');
    }
    str
}

Just slightly different, but still quick and easy. Ok, we can setup a simple main() to test these:

fn main() {
     let original = "Hello, World!";

     let encoded = encode(&original);
     println!("'{}' encodes as '{}'", &original, &encoded);

     let decoded = decode(&encoded);
     println!("which then decodes to '{}'", &decoded);
     assert_eq!(original, decoded);
 }



Command Line Args

Well, that was fun. Hey, lets change it to take what we’d like encoded from the command line. There a built-in library to help with that and, of course, we only need to change main()

fn main() {
     for arg in std::env::args().skip(1) {
       let encoded = encode(&arg);
       let decoded = decode(&encoded);
       assert_eq!(arg, decoded);
       println!("'{}' encodes as '{}' and decode works",
           &arg, &encoded);
     }
 }

So, std::env::args will give us all the args a program was called with, including the program name itself which is always in [0] and why we want to skip(1) to avoid encoding it. So now:

cargo run “Please sir, may I have some more soup”

runs just fine – I’ll avoid showing you the encoded version! Also:

cargo run What in the world

runs, and encodes each word separately, since they are individual parameters to the program.

Ok, more fun, what if we changed the + to 1 and the . to 0 and made this encoding look like binary, lol! Now, “Hello” encodes as:

11111111111111111111111111111111111111111111111111111111111111111111111101111111111111111111111111111101111111001110

I have more ideas of ways to play with this, so more to come next post…