14  Struct Methods

Objectives

Learn how to implement methods for stucts. Be able to create associated functions and understand the difference between Self, &self, and &mut self

Associated Functions

Typically methods are used on instantiations of a variable such as:

let me = Person { name: "Josiah".to_string(), age: 29 };
let birth_year = me.year_of_birth();

Sometimes it makes sense to use an associated function. These are functions that are called from the type itself e.g. Type::function(). They’re typically used for constructors. For example

let me = Person::new("Josiah".to_string(), 29);

They’re created using an impl block.

implement methods

impl is short for implement. We can implement methods for structs using the following syntax:

impl TypeName {
    fn method() {
        todo!()
    }
}

Associated functions are methods that do not reference the type itself. To create a new() constructor function that returns the type we can set the return type to Self:

impl Person {
    fn new(name: String, age: i32) -> Self {
        Person { name, age }
    }
}

This gives us the ability to use Person::new()

Self references

However, it often makes sense to call methods from an instantiation itself.

We can do this by setting the first argument to &self which provides a reference to the struct the method is called on.

impl Person {
    fn greet(&self) {
        println!("Hello, my name is {} and I am {} years old.", self.name, self.age);
    }
}

Using a &self reference means you cannot move the inner fields, only borrow them.

We can also have arguments that refer to other instantiations of the same type via &Self or Self. For example to compare ages of people:

impl Person {
    fn is_older_than(&self, other_person: &Self) -> bool {
        self.age > other_person.age
    }
}

Mutable self

Often it makes sense to modify the fields of a struct. This is when a mutable reference is handy via &mut self.

For example we can implement methods to rename() and celebrate_birthday():

impl Person {
    fn rename(&mut self, new_name: &str) {
        self.name = new_name.to_string();
    }

    fn celebrate_birthday(&mut self) {
        self.age += 1;
    }
}

Exercise

  • Implement a Point::new() associated function
    • Using arguments x: f64 and y: f64
    • It should return Self
  • Define a method euclidean_distance() which calculates the distance between itself and another Point
    • Use arguments &self and destination: &Self
    • Return f64
    • Distance is calculated as \(\sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}\)
    • Use .powi(2) to square a value and x.sqrt() to get the square root
  • In main.rs create two Point structs and calculate the distance between them.
View solution
impl Point {
    fn new(x: f64, y: f64) -> Self {
        Self { x, y }
    }

    fn euclidean_distance(&self, destination: &Self) -> f64 {
        let x_diff = (self.x - destination.x).powi(2);
        let y_diff = (self.y - destination.y).powi(2);

        (x_diff + y_diff).sqrt()
    }
}

fn main() {
    let a = Point::new(0.0, 5.0);
    let b = Point::new(-10.0, 1.5);
    let distance = a.euclidean_distance(&b);
    println!("Distance between a and b is {distance}");
}

Bonus: Exercise 2

Euclidean distance is useful only on a rectangle. What about on a sphere? Haversine distance is used to calculate the distance on a sphere. The Haversine formula computes the distance between two points on a sphere given their longitudes and latitudes.

It is defined by the following (messy) equation:

\[a = \sin^2\left(\frac{\Delta\phi}{2}\right) + \cos\phi_1 \cdot \cos\phi_2 \cdot \sin^2\left(\frac{\Delta\lambda}{2}\right)\]

\[c = 2 \cdot \text{atan2}(\sqrt{a}, \sqrt{1-a})\]

\[d = R \cdot c\]

Where:

  • \(\phi\) represents latitude (in radians) (use .to_radians())
  • \(\lambda\) represents longitude (in radians) (use .to_radians())
  • \(R\) is the Earth’s radius (you can use 6_371_008_7714f64 meters as a mean radius)
  • \(\Delta\phi\) is the difference in latitude between the two points (\(\phi_2 - \phi_1\))
  • \(\Delta\lambda\) is the difference in longitude between the two points (\(\lambda_2 - \lambda_1\))
  • \(\text{atan2}(y, x)\) is the arctangent of \(y/x\), using the signs of both arguments to determine the correct quadrant.

Hints for Implementation:

  • Remember to convert your latitude and longitude values from degrees to radians using the .to_radians() method on f64.
  • The c part of the formula, \(2 \cdot \text{atan2}(\sqrt{a}, \sqrt{1-a})\), can also be implemented using 2 * a.sqrt().asin() in Rust, which is a common simplification when a is within the valid domain for asin.
View solution
impl Point {
    fn haversine_distance(&self, destination: &Self) -> f64 {
        let radius = 6_371_008.7714; // Earth's mean radius in meters
        let theta1 = self.y.to_radians(); // Latitude of point 1
        let theta2 = destination.y.to_radians(); // Latitude of point 2
        let delta_theta = (destination.y - self.y).to_radians(); // Delta Latitude
        let delta_lambda = (destination.x - self.x).to_radians(); // Delta Longitude

        let a = (delta_theta / 2f64).sin().powi(2)
            + theta1.cos() * theta2.cos() * (delta_lambda / 2f64).sin().powi(2);

        2f64 * a.sqrt().asin() * radius
    }
}