Post

Rust is Shamelessly Object Oriented

Rust is Shamelessly Object Oriented

The Rust core community and maintainers wisely avoid unnecessary conflict to improve outreach and exaggerate differences to ensure people engage with them, but I suffer no such handicap!

If you find yourself saying Rust isn’t an object oriented language or critique a piece of code by commenting that’s the object oriented way of doing things, you’re wrong. If you find yourself swayed by, or even partially deferential to these points of view this post is also for you.

Why does this even matter?

First, I’ll admit a personal failing of mine. You’ve probably seen this famous XKCD cartoon duty calls.

For better or for worse, usually worse, I absolutely am this person. But, there’s more to it than a compulsive need to correct people. When people get this fact wrong, it influences how they write and review code in ways that degrade quality. Namely, if you write code that uses composition via traits (especially if anything is boxed), people operating under this misunderstanding will tend to object that composition isn’t idiomatic Rust.

Rust is object oriented

What is an object oriented language?

The object oriented programming paradigm is a class of approaches based on some notion of an object which can contain both code and data. Examples include class based models and prototype based models.

This is why Rust is clearly an object oriented language. In Rust we have structs which contain data and optionally bundled code which is why when you go to add functions the syntax is impl, it’s the code implementation of the struct:

1
2
3
4
5
6
7
8
9
10
11
// Data
struct Bar {
    age: u32,
}

// Code
impl Bar {
    pub fn print_age() {
        println!("Age: {}", age)
    }
}

Bar is an entity that contains both code and data, so it’s an object.

Dispatch method is orthogonal

First, let’s address the most common issue I’ve seen trip people up. From the perspective of this question, these are identical:

1
2
3
4
5
6
7
8
9
10
11
12
13
trait Thing {
    fn act();
}

// Static dispatch will be used
struct Foo<T: Thing> {
    thing: T,
}

// Dynamic dispatch will be used
struct Foo {
    thing: Box<dyn Thing>,
}

Both of these are object oriented and both are using composition. The difference is only in how the compiler will generate the code storing additional data and how it will perform function calls on the composed object. They’re both idiomatic Rust.

Dispatch method isn’t as important as people make it out to be! Check out StATiC DiSpATcH to learn more.

The competing definition appears baseless

Inheritance is just one of many features that may or may not be present, it isn’t required. Clearly, there is at least a myth floating around that it is subjective or up for debate. However, after a brief search I couldn’t find a single original source or justification for this belief. Every reference just says “some people think”. Who? Why!? If you know why, please reach out to me.

Until a proper substantiation is given, I’m comfortable calling the common definition in textbooks, Wikipedia, chat gpt, language comparison charts, etc the official definition. Some people think the Earth is flat. What people think is irrelevant, why they think it is everything.

The book plays nice to a degree that is unhelpful

If you read Chapter 17 of the book, what rings most clearly is that they’re not trying to get into a semantics debate. They’re focused on what Rust offers and how that maps against concepts the reader is already familiar with in other languages. I think doing that is wise overall, it avoids having a holy war.

Even putting aside how general the common definition of object oriented is, the old adage comes to mind. If it walks like a duck, quacks like a duck, looks like a duck, it’s a duck. Let’s review the subsections of the chapter.

Characteristics of Object-Oriented Languages

This section points out that usage of the term varies, but posits three commonalities exist: objects, encapsulation, and inheritance. It walks through each of these and the first two topics close with if you must have this to be OOP, know that Rust has it.

The exception obviously is inheritance, but the book goes on to explain that polymorphism isn’t 1:1 with inheritance and isn’t wishy-washy about it. Personally, I view having a strong stance on this term but saying arguably object oriented involves inheritance as a marketing choice. When they list the conditions, really the subtext is that by inheritance people actually mean polymorphism and that while Rust doesn’t have inheritance, it is polymorphic.

Using Trait Objects That Allow for Values of Different Types

This section introduces traits, how they fill the roles of inheritance and interfaces in other languages, and some specific notes on static vs dynamic dispatch as these concepts are probably new to most readers since in most managed languages polymorphic code is always dynamically dispatched.

While it isn’t addressing the OOP definition topic here, there’s an important snippet for properly interpreting the next section. Keep this in mind:

We’ve mentioned that, in Rust, we refrain from calling structs and enums “objects” to distinguish them from other languages’ objects. 1

Implementing an Object-Oriented Design Pattern

This is the element of truth that is sometimes found in these types of code comments. If you copy your Java or C# or C++ code wholesale just dropping in a trait wherever you had an interface (or stateless abstract parent class in C++) you’re not doing things “the Rust way”.

Here’s the first foul:

The state objects share functionality: in Rust, of course, we use structs and traits rather than objects and inheritance.

This contradicts the earlier notion that polymorphism is not inheritance, or at a minimum is a switch from the flexible definitions to a prescriptive one where inheritance is a necessary element of being object oriented. It’s also misleading because it doesn’t mention that Enums can also implement traits! This is why the mapping / distinctions made along the struct vs “object” line aren’t good. It presents clear mappings of alternatives, when in practice it’s more subtle than that.

The key point this section is making is common mutable state based patterns with getters, setters, or complicated action abstractions over state mutations often aren’t the best or most idiomatic way to do things in Rust. The killer feature Rust offers in this area is its algebraic type system, which is why the book suggests Encoding States and Behaviors as Types as the preferred alternative.

Regrettably, it also makes a vague passing comment about using macros to reduce duplication. This is meant to advertise a feature which is good, but I’ve also seen people take away from this that instead of “doing OOP” they should write macros and end up coming up with strange proc macros that ironically end up reinventing inheritance in the process!

Wrapping up

You may have noticed my language got softer and more nuanced as the post went on. This was an intentional choice. The reason I took a strong claim here is that as mentioned it’s the commonly accepted definition, and quibbling over it is missing the point. I firmly believe that the combination of presenting the definition of OOP as flexible combined with wanting to distinguish Rust terms from similar concepts in other languages ends up confusing people.

In isolation each statement is fine, but the mix without clear fence posting creates room for confusion that I’ve seen manifest in bad code first hand. Traits aren’t “different” from interfaces, they’re superior supersets of them.

  1. Some readers may find this to be a dishonest quote, as the text immediately following it seems to contradict my earlier claim about structs. For completeness, it goes on:

    In a struct or enum, the data in the struct fields and the behavior in impl blocks are separated, whereas in other languages, the data and behavior combined into one concept is often labeled an object. However, trait objects are more like objects in other languages in the sense that they combine data and behavior. But trait objects differ from traditional objects in that we can’t add data to a trait object. Trait objects aren’t as generally useful as objects in other languages: their specific purpose is to allow abstraction across common behavior.

    The reason I pulled this part out separately is because it distracts from the main point which is more easily outlined by setting this aside for a moment. I believe that this section is more responsible for the confusion we see on this topic than anything else.

    In this context what they’re calling object would almost always be better labeled as a class. In such languages, there isn’t always a distinction between struct and class. Rust chooses to make the distinction by the presence or absence of an impl block, but this isn’t a categorical difference. Implementations are kept separate because it avoids the need for partial class like syntax, allows external definitions (like blank implementations and derive macros) to be defined, and just organizes everything better.

    Nothing is different about this organization when it comes time to generate code for the object or for the programmer that is using it. Data and code aren’t combined in compiled code. Similarly, the distinction made on trait objects is misguided in my opinion. The exact same thing is true for interfaces vs inheritance in other languages, and personally I dislike the characterization that interfaces are less useful. To say this is to tacitly suggest not supporting inheritance was a mistake. ↩︎

All rights reserved by the author.