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.