5. Functions and Behavior#

Learn how to define actions and make your code work with different data types.

5.1. How do I define my own actions and operations, and how do they work with different data types?#

Basic functions:

fn add_numbers(a: i32, b: i32) -> i32 {
    a + b  // Last expression is returned (no semicolon)
}

fn greet(name: &str) {
    println!("Hello, {}!", name);  // No return value
}

fn main() {
    let result = add_numbers(5, 3);
    println!("5 + 3 = {}", result);

    greet("Alice");
}

Compare to C++:

int add_numbers(int a, int b) {
    return a + b;
}

void greet(const std::string& name) {
    std::cout << "Hello, " << name << "!" << std::endl;
}

Compare to Python:

def add_numbers(a, b):
    return a + b

def greet(name):
    print(f"Hello, {name}!")

Adding actions to your custom data types:

struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    // Associated function (like a static method)
    fn new(width: f64, height: f64) -> Rectangle {
        Rectangle { width, height }
    }

    // Method that reads data
    fn area(&self) -> f64 {
        self.width * self.height
    }

    // Method that modifies data
    fn scale(&mut self, factor: f64) {
        self.width *= factor;
        self.height *= factor;
    }

    // Method that consumes the data
    fn into_square(self) -> Rectangle {
        let side = (self.width + self.height) / 2.0;
        Rectangle::new(side, side)
    }
}

fn main() {
    // Create using associated function
    let mut rect = Rectangle::new(10.0, 5.0);

    // Call methods
    println!("Area: {}", rect.area());

    rect.scale(2.0);
    println!("After scaling, area: {}", rect.area());

    let square = rect.into_square();
    // rect is no longer usable here
}

Compare to C++ classes:

class Rectangle {
private:
    double width, height;

public:
    Rectangle(double w, double h) : width(w), height(h) {}

    double area() const {
        return width * height;
    }

    void scale(double factor) {
        width *= factor;
        height *= factor;
    }
};

Compare to Python classes:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def scale(self, factor):
        self.width *= factor
        self.height *= factor

Functions are defined with fn and method collections are defined with impl.

5.2. How can I make my code work with different data types in a general way, similar to C++ templates or Python’s duck typing?#

You can define shared behaviors that any type can implement. This allows writing flexible functions that work with many different types.

Think of it like defining a contract: “Any type that can do X can be used here.”

Defining a shared behavior:

trait Drawable {
    fn draw(&self);
    fn area(&self) -> f64;
}

struct Circle {
    radius: f64,
}

struct Rectangle {
    width: f64,
    height: f64,
}

// Circle implements the Drawable behavior
impl Drawable for Circle {
    fn draw(&self) {
        println!("Drawing a circle with radius {}", self.radius);
    }

    fn area(&self) -> f64 {
        3.14159 * self.radius * self.radius
    }
}

// Rectangle implements the Drawable behavior
impl Drawable for Rectangle {
    fn draw(&self) {
        println!("Drawing a rectangle {}x{}", self.width, self.height);
    }

    fn area(&self) -> f64 {
        self.width * self.height
    }
}

// Function that works with any Drawable type
fn display_shape(shape: &dyn Drawable) {
    shape.draw();
    println!("Area: {}", shape.area());
}

fn main() {
    let circle = Circle { radius: 5.0 };
    let rectangle = Rectangle { width: 10.0, height: 5.0 };

    display_shape(&circle);     // Works with Circle
    display_shape(&rectangle);  // Works with Rectangle
}

Generic functions (like C++ templates):

// Function that works with any type that implements Drawable
fn print_area<T: Drawable>(shape: &T) {
    println!("The area is: {}", shape.area());
}

// Generic function with multiple constraints
fn compare_areas<T: Drawable, U: Drawable>(shape1: &T, shape2: &U) {
    if shape1.area() > shape2.area() {
        println!("First shape is larger");
    } else {
        println!("Second shape is larger");
    }
}

fn main() {
    let circle = Circle { radius: 5.0 };
    let rectangle = Rectangle { width: 10.0, height: 5.0 };

    print_area(&circle);
    print_area(&rectangle);
    compare_areas(&circle, &rectangle);
}

Compare to C++ templates:

template<typename T>
concept Drawable = requires(T t) {
    t.draw();
    { t.area() } -> std::convertible_to<double>;
};

template<Drawable T>
void print_area(const T& shape) {
    std::cout << "Area: " << shape.area() << std::endl;
}

// Or without concepts (older C++)
template<typename T>
void print_area(const T& shape) {
    std::cout << "Area: " << shape.area() << std::endl;
}

Compare to Python duck typing:

# Python doesn't enforce interfaces at compile time
def display_shape(shape):
    shape.draw()  # Works if shape has draw() method
    print(f"Area: {shape.area()}")  # Runtime error if no area() method

def print_area(shape):
    print(f"Area: {shape.area()}")  # "If it walks like a duck..."

Key advantages:

  • Compile-time checking ensures methods exist

  • No runtime performance cost

  • Clear contracts for what types can do

  • Composable behaviors

This shared behavior system is called trait.