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

Rust Questions from Beginners, Including Me

Have Rust beginner questions? Discord has Rust channels for the help you’re looking for!

Beginners are encouraged to ask questions
We are stocked full of answers. But… some are “no”.

Information overload and I’m still trying to find a bigger project I can work on that interests me. I have the Interpreter to work on still, and that will take some serious work! But I’m also thinking of going back to small systems of my programming past and playing with writing them in Rust. In the meantime, I thought I’d sneak around the Rust #beginners channel on Discord and give everyone a peak at some questions (and hopefully some answers) that beginners are asking about their early Rust code.


I have a [u8; 10] and I have tried to do a .map() on it but I get:

note: the method 'map' exists but the following trait bounds were not satisfied:
   '&mut [u8; 10] : std::iter::Iterator'
   '&mut [u8] : std::iter::Iterator'

Try .iter().map(...) (Or iter_mut based on what you need).

So, I’m assuming this is an example of that (though, with u16 so my sum doesn’t get too big):

fn main() {
    let list: Vec<u16> = [1,1,2,3,5,8,13,21,34,55].to_vec();
    list.iter().for_each(|n| println!("The next is: {}", n));
    let sum: u16 = list.iter().map(|n| { n }).sum();
    println!("\nSum of list: {}", sum);
}

Note a few things here. We couldn’t do:
list.iter().map(|n| println!("The next is: {}", n));
and the compiler nicely reminds us why:
note: iterators are lazy and do nothing unless consumed

So, iter().for_each() closure works nicely … or we could have used an old-school for loop over the iter() instead.


What would be the Rust equivalent for this in C:
#define TEST 1

A constant is appropriate for this case, for example:
const TEST: i32 = 1;




Student looking in book for answers to his questions
I. Don’t. See. My. Question. Anywhere!

I went ahead and asked my beginner question from the previous blog post: when implementing a trait for several “related” structs, I can require other traits that I depend on… but I can’t make a field (that all the structs share) a requirement… which prevents me from being able to write a default implementation of a trait – I have to write it individually for each struct… is that “just how it is”?

And I got back a good answer:

struct fields are not part of a trait's interface; traits can be implemented by things other than structs e.g. enums, primitive types, closures, …

you can add a health(&self) -> u8 method on your trait that each struct is required to implement, and then use that in the trait's implementation of cur_health

if you want the trait to advertise that its implementors have a concept of "health" then that's one way to go about it

All of that makes sense… and leads me to slightly change the code, to see how it looks with that idea…

struct Player {
    player_name: String,
    real_name: String,
    level: u8,
    health: u8,
}

struct Monster {
    name: String,
    subtype: String,
    hit_dice: u8,
    health: u8,
}

trait Creature {
    fn health(&self) -> u8; 
    fn cur_health(&self) -> String {
        format!("Health: {}", self.health())
    }
}

impl Creature for Player {
    fn health(&self) -> u8 { self.health }
}
impl Creature for Monster {
    fn health(&self) -> u8 { self.health }
}

fn main() {
    let player_1 = Player {
        player_name: "Aragorn".to_string(),
        real_name: "Viggo Mortensen".to_string(),
        level: 20,
        health: 120,
    };
    let monster_1 = Monster {
        name: "Orc".to_string(),
        subtype: "Captain".to_string(),
        hit_dice: 7,
        health: 21,
    };

    println!(" Player 1: {}", player_1.cur_health());
    println!("Monster 1: {}", monster_1.cur_health());
}

Now, health() is a required trait, and not default implemented (since I can’t), but cur_health() to print the string now CAN be defaulted.

So I can implement the simple health() for each struct and let the “fancy” cur_health() default for each struct type. I do think this is an improvement. I’m not sure I’d even implement any of this in this way, but this example is showing me what is possible.

Rust Functions, Methods, and Traits

Where I’m given too many choices and I bounce around weird code examples

There’s structure here…

Bear with me while we work on the silly insides of a game. I was having trouble contriving of examples to explain methods and traits and I fumbled around a bit. I think that’s how mushy the difference is for me at the moment. I’m going to give it a try anyway – hold on tight, I’m going to try to explain Rust functions, methods, and traits.

Functions

Let’s start each section with a quote from the Rust book, so I make sure to get this right. Webopedia defines a function as “a named section of a program that performs a specific task … a type of procedure or routine.” Typically, functions perform some aspect of your application or some internal function, but not some specific action against or for a defined object – those are more likely methods or traits.

We’ve seen some simple functions in previous blog posts for reversing a string, or this other one, checking if a given year is a leap year:

 fn is_leap_year(year: u64) -> bool {
  let is_divisible = |n| { year % n == 0 };

  is_divisible(4) && (!is_divisible(100) || is_divisible(400))
}

The function name here is is_leap_year and it expects a unsigned, 64-bit number to be passed in as the year you’d like to check, and returns a simple boolean answer: true or false. For instance, you could call:

let leap_year = is_leap_year(2004);
println!("2004 is a leap year: {}", leap_year);

leap_year would be equal to (and print as) true for 2004, and false for 2003, for example. I know, this is all very simple so far.




Methods

Webopedia says a method is “(in object-oriented programming), a procedure that is executed when an object receives a message. A method is really the same as a procedure, function, or routine in procedural programming languages.” So, think of the actions that can happen TO an object. For instance, if you have a data structure for an employee, that employee instance could be assigned a salary, or something might want to get the employee‘s name.

You describe a method (a function OF an object) with impl. That is, you implement a function for a class. In the code below, I’ll define a custom data structure for storing an employee’s data: their first and last names, and their salary. Then, I’ll implement 3 methods that can work against a single employee: to print their name normally; to print their name last, first; and to set their new salary.

#[derive(Debug)]
struct Employee {
    first_name: String,
    last_name: String,
    salary: f32,
}

impl Employee {
    fn full_name(&self) -> String {
        format!("{} {}", self.first_name, self.last_name)
    }
    fn sort_name(&self) -> String {
        format!("{}, {}", self.last_name, self.first_name)
    }
    fn set_salary(&mut self, new_salary: f32) {
        self.salary = new_salary;
    }
}

fn main() {
    let mut developer = Employee {
        first_name: "Thomas".to_string(),
        last_name: "Anderson".to_string(),
        salary: 0.0,
    };

    println!("{:?}", developer);
    println!("Full Name: {}", developer.full_name());
    println!("Sort Name: {}", developer.sort_name());
    developer.set_salary(1_000_000.00);
    println!("{:?}", developer);
}

So, I derive the Debug trait (yeah, trait… see below) so that Rust knows how to dump my structure to the screen with a debug print {:?}. Then I describe 3 functions, just like normal, except they are inside a impl block for the struct Employee. The other difference is that they all take an input parameter called &self. That IS the object itself, so you know WHICH instance of employee you are working on. In main() you can see my set up a single employee, and call these methods. The output looks like:

Employee { first_name: "Thomas", last_name: "Anderson", salary: 0.0 }
Full Name: Thomas Anderson
Sort Name: Anderson, Thomas
Employee { first_name: "Thomas", last_name: "Anderson", salary: 1000000.0 }

Traits

I had to go to the Rust book for a definition of Traits: “A trait tells the Rust compiler about functionality a particular type has and can share with other types. We can use traits to define shared behavior in an abstract way.” So, think about the ways we can output information: we can print to the screen, or write the data to a file, or spit the data out a network connection, or send it to the printer. That is the same “idea”, implemented different ways for different situations. Traits are good for that. I went a little crazy for this example, but here’s some code first:

struct Player {
    player_name: String,
    real_name: String,
    level: u8,
    health: u8,
}

struct Monster {
    name: String,
    subtype: String,
    hit_dice: u8,
    health: u8,
}

struct NPC {
    name: String,
    position: String,
    health: u8,
}

struct Villager {
    health: u8,
}

trait Creature {
    fn cur_health(&self) -> String;
    fn equiv_level(&self) -> String;
    fn game_name(&self) -> String {
        format!("Random villager")
    }
    fn display_name(&self) -> String {
        format!("{}, at {} health", self.game_name(), self.cur_health())
    }
    fn scoreboard_name(&self) -> String {
        format!("{:.<20.20}..[lv:{:>03}]: {:>8} health", self.game_name(), self.equiv_level(), self.cur_health())
    }
}

impl Creature for Player {
    fn cur_health(&self) -> String {
        self.health.to_string()
    }
    fn equiv_level(&self) -> String {
        self.level.to_string()
    }
    fn game_name(&self) -> String {
        format!("{}", self.player_name)
    }
    fn display_name(&self) -> String {
        format!(
            "{} [level {}], played by {}, at {} health",
            self.game_name(), self.level, self.real_name, self.cur_health()
        )
    }
}

impl Creature for Monster {
    fn cur_health(&self) -> String {
        self.health.to_string()
    }
    fn equiv_level(&self) -> String {
        self.hit_dice.to_string()
    }
    fn game_name(&self) -> String {
        format!("{}, {}", self.name, self.subtype)
    }
    fn display_name(&self) -> String {
        format!(
            "{} [hit dice {}] at {} health",
            self.game_name(), self.hit_dice, self.cur_health()
        )
    }
}

impl Creature for NPC {
    fn cur_health(&self) -> String {
        self.health.to_string()
    }
    fn equiv_level(&self) -> String {
        "1".to_string()
    }
    fn game_name(&self) -> String {
        format!("NPC: {}, {}", self.name, self.position)
    }
    fn display_name(&self) -> String {
        format!("{} at {} health", self.game_name(), self.cur_health())
    }
}

impl Creature for Villager {
    fn cur_health(&self) -> String {
        self.health.to_string()
    }
    fn equiv_level(&self) -> String {
        "1".to_string()
    }
}

fn main() {
    let player_1 = Player {
        player_name: "Aragorn".to_string(),
        real_name: "Viggo Mortensen".to_string(),
        level: 20,
        health: 120,
    };
    let monster_1 = Monster {
        name: "Orc".to_string(),
        subtype: "Captain".to_string(),
        hit_dice: 7,
        health: 21,
    };
    let npc_1 = NPC {
        name: "Butterburr".to_string(),
        position: "Innkeeper".to_string(),
        health: 6,
    };
    let villager = Villager {
        health: 1,
    };

    println!("\nINSIDE THE ROOM:");
    println!("       Player 1: {}", player_1.game_name());
    println!("      Monster 1: {}", monster_1.game_name());
    println!("          NPC 1: {}", npc_1.game_name());
    println!("     Villager 1: {}", villager.game_name());

    println!("\n        DETAILS:");
    println!("       Player 1: {}", player_1.display_name());
    println!("      Monster 1: {}", monster_1.display_name());
    println!("          NPC 1: {}", npc_1.display_name());
    println!("     Villager 1: {}", villager.display_name());

    println!("\n     SCOREBOARD:");
    println!("       Player 1: {}", player_1.scoreboard_name());
    println!("      Monster 1: {}", monster_1.scoreboard_name());
    println!("          NPC 1: {}", npc_1.scoreboard_name());
    println!("     Villager 1: {}", villager.scoreboard_name());
}
En garde!

So, in this imaginary game, we have players, monsters, NPCs (non-player characters), and villagers. All of these “creatures” will be wandering around the countryside and interacting with each other. There needs to be a way to identify each of these different types of creatures, even though they will eventually have VERY different aspects from each other. For instance, I might need just their names… or maybe some detailed information… or I might need their name, level, and health to put in the top corner of the game screen. The different types of output looks like this:

INSIDE THE ROOM:
       Player 1: Aragorn
      Monster 1: Orc, Captain
          NPC 1: NPC: Butterburr, Innkeeper
     Villager 1: Random villager

        DETAILS:
       Player 1: Aragorn [level 20], played by Viggo Mortensen, at 120 health
      Monster 1: Orc, Captain [hit dice 7] at 21 health
          NPC 1: NPC: Butterburr, Innkeeper at 6 health
     Villager 1: Random villager, at 1 health

     SCOREBOARD:
       Player 1: Aragorn...............[lv: 20]:      120 health
      Monster 1: Orc, Captain..........[lv:  7]:       21 health
          NPC 1: NPC: Butterburr, Inn..[lv:  1]:        6 health
     Villager 1: Random villager.......[lv:  1]:        1 health

So, as the coder, I decided that each of these (player, monster, NPC, and villager) share some similarities, which I’m going to include as traits. The trait setup itself, can simply identify what any object that includes this trait MUST define for itself. Plus, the trait can even include a default implementation of some of those traits. In this example, I setup game_name() to default to the string “Random villager”. If you don’t override that trait, that’s what you’ll get. But you can see I did override it for the Player type, Monster type, and the NPC. I didn’t even define it for the Villager, because the default implementation is all I needed. On the other hand, the scoreboard_name() default works for ALL of my types. And there is no default for cur_health(), so it must be defined for each type – the compiler won’t let me skip it!

It would be nice if I could setup the default cur_health to be:

    fn cur_health(&self) -> String {
        self.health.to_string()
    }

since that is what they ALL are. But the generic Trait setup doesn’t know what is coming in as &self, so it has no idea what self.health could mean or if it will even be defined. So, I don’t think there’s a way around this. On the other hand, the scoreboard_name() default relies on all the other traits being setup, so it CAN be defined as a default and not require setup anywhere else.

Traits let you define an expected functionality and anything that adopts that functionality MUST define how it functions for that specific data structure, unless there is a default implementation that works like you need it. Rust will, therefore, require you setup everything and not forget one.

It’s going to take me awhile to understand what I should develop as a trait vs method. I’m sure with practice, it will make more sense. It DOES make sense for the built-in traits, for example to add two things together. It is a trait to .add_to which had to be written for u8 and u16 and f32, etc. They could use generic types to write it once and save a bunch of repeating code. It also makes sense, if I were to make a Point struct or a Complex_Number struct, that I could implement the .add_to trait for those so I could add my Points together. So, extending existing traits in the system to my own data structures seems very helpful – when it makes sense, of course… it doesn’t make sense to add Monsters together.