Rust: Traits

14 February 2023

A trait is an interface that types can implement. It consists of a set of associated items, which can be types, constants, functions, and methods. As an example, let’s take a look at a trait used for logging messages with a log level:

trait Logger {
    // associated type
    type Level;

    // associated constant
    const DEFAULT_LEVEL: Self::Level;

    // associated function
    fn new() -> Self;

    // method
    fn log(&self, msg: &str, level: Self::Level);

    // method with default implementation
    fn log_error(&self, msg: &str) {
        self.log(msg, Self::DEFAULT_LEVEL);
    }

    // mutating method
    fn set_minimum_level(&mut self, level: Self::Level);
}

The items in a trait can be either declarations, meaning constants without a value or functions without a function body, or definitions. A definition provides a default value or implementation that an implementation of the trait may or may not override. Types are always declarations, though.

In a trait definition, Self stands for the implementing type.

Let’s implement the trait. We’ll need two types here, one that we’ll assign to Level and the one that will actually implement Logger:

#[derive(Debug, PartialEq, PartialOrd)]
enum SimpleLevel {
    Debug,
    Info,
    Error,
}

struct SimpleLogger {
    minimum_level: SimpleLevel,
}

To implement the trait we need a trait implementation that includes a definition for each item in the trait (the ones with a default implementation are optional):

impl Logger for SimpleLogger {
    type Level = SimpleLevel;

    const DEFAULT_LEVEL: SimpleLevel = SimpleLevel::Debug;

    fn new() -> SimpleLogger {
        Self {
            minimum_level: Self::DEFAULT_LEVEL,
        }
    }

    fn log(&self, msg: &str, level: SimpleLevel) {
        if level < self.minimum_level {
            return;
        }
        println!("[{:?}] {}", level, msg);
    }

    fn set_minimum_level(&mut self, level: SimpleLevel) {
        self.minimum_level = level;
    }
}

This looks pretty similar to the inherent implementations in the previous article, Rust: Methods, except we specify the trait name at the top.

Here’s some code using the function and methods from the trait:

let mut logger = SimpleLogger::new();
logger.log("Hello!", SimpleLevel::Info); // prints: [Info] Hello!
logger.set_minimum_level(SimpleLevel::Error);
logger.log("Hello!", SimpleLevel::Info); // does not print message