Rust Traits: Engines of Extensibility
By Tyler
In my last article I wrote about Rust Traits as an alternative way of looking at abstraction, as opposed to the traditional OOP paradigms. In this article I want to write about how Rust Traits, and how they provide extensibility for the rust language
Extensibility in the physical domain
To set the foundation I want to start with an Analogy. Consider the ever popular children’s toy, Legos. My kids love Legos, I remember loving Legos when I was a kid as well. The other day I was contemplating on why Legos are so popular? The reason I came up with is that Lego’s simplicity, allows for creativity and exploration on the part of the user. In short Legos are extensible.
First of all, Legos are (for the most part) very simple. They have pips that interlock with corresponding grips on other Legos. Thats about it. Blocks can come in various sizes, and shapes, but they all fit the same basic interface. It’s this simplicity that allows for extensibility. The simplicity of the building blocks makes learning how to use them quick and easy but at the same time it does not hinder a child’s creativity. Legos may ship with instructions on how to build certain designs, but nothing about the building blocks themselves limits their usability to those designs.
When it comes to software, programmers are no different than kids with Legos (I’d venture to guess that many still play with them too). Programmers like to be able to compose bits of software together to produce vastly different tools and applications.
Programming Languages are building blocks
Every (modern) programming language is a set of Legos. They provide the basic building blocks with which software is developed. They provide loops, branching logic, and the ability to create subroutines, and store variables. Most languages also provide basic data structures for use (arrays, lists, mappings, strings), and basic functionality to deal with them.
However some languages provide further extensibility by providing simple interfaces for these core features. Lets look at Python for an example. Python provides their “Data Model” as an interface to core python features. By implementing different interfaces on your objects, you make those objects accessible to those same core features.
For example lets look at looping. Pythons loops work on iterators
.
Reading in the documentation
you can see that an iterators can be made by adding a __iter__()
and __next__()
method on your class.
class ExampleIterator:
__iter__(self):
return self
__next__(self):
return 1
for x in ExampleIterator():
print("This will loop forever")
In this example, we’ve created a simple iterator that will infinitely generate the value 1. Not very useful, but still incredibly powerful that we can create custom objects that will seamlessly work with the core features of our language.
Protocols and Traits
One thing you might have noticed about the above example is that in order to make my class an iterable, I didn’t have to subclass some base Iterator type. I simply had to add a few methods to a declared interface. In Python’s docs, you’ll see this referred to as a “protocol”, which is a very good word (and the technical way) to describe it. A protocol is an agreement to behave in a certain way, under certain circumstances. These sorts of agreements between programmers form the building blocks of all sorts of software.
If you think that sounds a bit like Rust Traits, then you might just fit
in with the Rust Language Developers. In Rust, traits are the engine of language extensibility.
Want to be able to loop over one of your structures? Implement the iterator
trait.
use std::iter::Iterator;
pub struct ExampleIterator {};
impl Iterator for ExampleIterator {
type Item: u8
fn next(&mut self)-> Option<Self::Item>{
return Some(1);
}
}
Here we have implemented the same kind of iterator as our above python example, which will forever return a value of 1 (technically in this case an instance of the Option Enum, but that is beyond the scope of this discussion).
there is one way, however, that I think rust’s trait system is superior to python’s data model. If I make an object that should be an iterator, but forget to implement the iterator protocol, Python will have no issues with it until run-time. Because rusts traits are part of the compile time checks, it means that the protocols they outline are enforced by the compiler. Protocols implemented as rust traits become more than simple agreements, but enforced constraints on our system, which can be checked.
Take Home message
So far this discussion, you might think, hasn’t touted traits, but languages that implement these interfaces into their core functionality. This is partly by design. Philosophically I rarely think core ideas like “protocols” and “extensibility” are captured uniquely by one language over another. As illustrated in this article, python has implemented protocols to extend core language features beautifully, all without access to rust-like traits.
And that is the beauty of learning new languages. Each language has a unique take on different aspects of programming, and by learning them we can start to see how those ideas could be integrated back into languages we already know.