Rust Traits Are Not Interfaces

Rust Traits Are Not Interfaces

May 30, 2023

Introduction

In constrast with today’s major programming languages (e.g. Python, C++, Java and, to some extent, JavaScript and TypeScript), Rust does not have the concept of an object. Instead, it uses structs, which are just collections of named attributes whose names and types are predefined, just like the plain old structs in C. Methods and other associated items can be added in two ways: through inherent implementations or through traits.

Inherent implementations are specific to a single struct, while traits define behaviours and characteristics that, in principle, are shared by multiple structs. The items defined inside them are called associated items and can be functions, methods and constants for inherent implementations, while traits can define also associated type aliases.

Throughout this article I’ll assume that the reader is familiar with the basics of structs and traits in Rust, and we’ll concentrate on traits and their superficial similarity with interfaces.

Traits Define Independent Namespaces

For a newcomer traits might resamble what in object oriented programming languages are called interfaces, because they define a set of methods and other associated items and are not inherited. However, the similarity ends here: traits are more powerful than interfaces and provide richer semantics, because traits define namespaces that are completely independent from each other. Therefore, if necessary, two traits can define methods with the same name, signature and/or implementation, up to having completely identical bodies.

This property is useful because, for example, different traits might need to implement the same method with the same signature, but assing different semantics to it. A concrete example of this situation can be found in the std::fmt module of the standard library, which deals with the formatting and printing of strings. Among other things, it defines the two most commonly used formatting traits, Debug and Display. They are both used to print structs, but in different contexts: the first is intended to be used for debugging and the second for pretty prints. What’s peculiar about them is that they have the exact same definition, except for the name:

use std::fmt::{Formatter, Error};

pub trait Debug {
    // Required method
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}
pub trait Display {
    // Required method
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}

Another consequence of the fact that traits define independent namespaces is that it is not sufficient for a struct to provide the required associated items in order for it to implement a trait: we also have to specify that the said items are associated with the trait. This is in stark contrast with interfaces, which objects have to implement, but do not have to declare explicity.

For example, in order to implement the Display trait for the Foo struct we would have to write

impl Display for Foo {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
        // Implementation goes here
    }
}

while for the Python equivalent of this protocol is sufficient to implement the __str__() method:

class Foo:
    # ...

    def __str__(self) -> str:
        # implementation goes here

Disambiguation Rules

If a given struct may have different definitions of the same associated item, how can we avoid ambiguities? Rust adheres to the following rules:

  • if a given name appears only once among the traits and the impl block, then that single occurence will be used.

  • If a name appears in multiple traits, but not in the impl block, then we will have to disambiguate using the Fully Qualified Syntax or the compiler will throw an error. The general syntax for this disambiguation is <value as Trait>::foo.

    The only expection are methods: given that they accept a reference to self as their first argument, if we were to write Trait::method_name(value) then in principle would be possible to determine which trait of which struct we are calling. Accordingly, the compilier supports both syntaxes and you are free to employ the one you find the most appropriate.

  • If an identifier is present both in the impl block and in some traits and we do not specify anything Rust will default to one from the impl block. In order to use the one from the traits we will have to use the fully qualitfied syntax.

Conclusions

Instead of being just another iteration on preexisting mainstream concepts, Rust is able to bring toghether things that where previously present only in more niche languages into a single platform. This make it more difficult than usual for newcomers to become accustomed to the language, but also more rewarding once the initial effort starts to pay off. The unique properties of traits are one of such new concepts.

In my personal experience I rarely find myself having to deal with overlapping traits, but knowing that traits define independent namespaces is important for developing a full understanding of how traits work, which in turn is necessary to understand Rust iteself and also why many packages are designed in the way they are. The earlier one learns it, the soon he or she will start to reap the benefits bentioned above.

Resources