Rust traits: an irresistible language feature
By Tyler
Rust Traits
If you read any article about Rust, you would most likely see three features discussed as what makes it different from langauges like C, Java, or Python, etc.: default immutability, its saftey features, and traits. While each of those features are powerful, it is Rust’s traits that I find the most alluring.
What are Traits?
Traits are Rust’s main abstraction mechanism. If you wanted to create an interface in Python, you would have to create an Abstract Base Class, and then inherit from it for implementations. Rust does not have classes. Rather it has structs - collections of data - and then you can implement traits on those structs. It is a system very similar to using mixins with multiple inheritance.
So for example, lets say we wanted to make an interface to make queries from a database. In Python you might write:
import abc
class Selectable(abc.ABC):
@abc.abstractmethod
def select(query, params):
pass
# Inherit from selectable ( And other Mixins as desired )
class PostgresDB(Selectable):
def select(query, params):
# Implementation
In Rust it would look like this:
pub trait Selectable {
// Ignore SelectResult, Rust is strongly typed, and requires
// return types to be defined
fn select(query: &str, params: Vec<&str>) -> SelectResult
}
struct PostgresDB {
// Data
}
impl Selectable for PostgresDB {
fn select(query: &str, params: Vec<&str>) -> SelectResult {
// Implementation
}
}
On Data modeling and abstraction
Now why would you want to use traits? To understand, we need to discuss inheritance, abstraction and data modeling. When modeling objects, you are confronted with the choice to inherit (sub-class) or compose (use one class in another) when describing relationships between two objects. The rule of thumb is to ask “Is this relationship an ‘Is-A’ or ‘Has-A’” relationship?"
To use an cliché example: a Car is a Vehicle, but a Car has Wheels. Sounds reasonable. I’d say it is, otherwise there wouldn’t be tens of thousands of highly intelligent programmers using the paradigm. There are two (pertinent) issues however, that arise from this framework of thinking.
First is that thinking about inheritance in this way often confuses code re-use
(aka: non-duplication, or Do Not Repeat Yourself ) as the main
objective of inheritance. It is not. The primary benefit of inheritance is
abstraction. When you inherit from a class ( or implement an interface ), you
are able to generalize your code in other places. To use our example: Rather
than hard coding a reference to a Car
, we can use Vehicle
, and give
ourselves the opportunity later on to use a class other than Car
, as long as
it is an automobile (say for example a Truck
).
Dangers of coupling data
So if abstraction, is the main objective, what is the issue with ‘Is-A’ vs ‘Has-A’ thinking? The answer is data coupling.
Lets continue with our cliché. Suppose we are designing a program for a
hyper-realistic video game. Our team has been tasked with designing the vehicles
for characters to travel in. We might suppose that all vehicles have wheels, and
so that we should put the wheels
attribute in the Vehicle
parent class, so
that all child classes can have access to it. Nice! we eliminated some possible
duplication. But then Bob, our co-worker who breaks all our stuff, decides that
since boats are also vehicles, he should subclass from Vehicle
when
implementing his Yacht
. And now you have a boat that has a wheels
attribute.
Now you could go back and change the inheritance structure to now have a
WheeledVehicle
class that inherits from Vehicle
. Bob can now have his boat,
and not have to worry about wheels getting in the way. This may seem fine, but
as your codebase grows, the branches of your codes family tree will grow until
it looks more like a biological taxonomy of Kingdoms an Phylae than a coherent
architecture.
Abstracting over behavior
The problem here is that your sub-classes are sharing data, and not just implementing behaviors. And this leads us to the second problem. Often when modeling, we say something ‘is’ a certain way, what we actually mean is that it ‘behaves’ a certain way (read in the deeper philosophical meaning you see fit). This is where traits (or mixin classes, in other languages) come in.
When you implement a Trait, you are not making assumptions about what something
is, just what it can do. Later in your code, you only need specify that you need
a structure that implements that trait. In short you don’t care what kind of
object it is, just what it can do. This can even be represented when naming
your traits. Rather than having a ReadableDatabase, you can just implement
Readable
(or Selectable
as we did).
To refer back to our original example, when we abstracted a database for a query
operations we simply said that it must have a select
method. If we decide that
we want to use an in-memory (e.g Redis) solution rather than a traditional
relational database, we can implement an interface to it by simply making sure
to implement the Selectable
trait. No need to worry about data specific to a
relational databases, leaking into our Redis implementation, and we don’t have to
reorder the hierarchy of our code.
So What?
The benefit of working with code in this way is that it makes it easier to write your code in a more flexible and testable way. The focus on behavior makes you think about how the system you are modeling works generally and not worry about implementation details of what bits of data it will need. It makes it easier to test your code, as it makes it easy to create mock implementations of the resources you need so that you can focus on testing core logic.
I’ll readily admit that there is nothing revolutionary about traits, when it comes to what I’ve written about here. The above benefits simply come from abstraction and having a mindset towards flexibility when designing your software. However Rust’s trait system makes it much easier to be in this mindset because it primes your mind to think about behavior over nature.
Leave them wanting more
They say it’s always good to leave them wanting more. Originally I had planned this to be a single article, but as I got into the discussion on abstraction, it became apparent that the subject would be benefited by being split up. I imagine that in the end that this will be the first of three articles on Rust’s Traits. The next article will discuss traits and language extensibility.