Object-Oriented Patterns in Auto
Object-oriented programming (OOP) is a way of modeling programs. Objects as a programmatic concept were introduced in the programming language Simula in the 1960s. Many competing definitions describe what OOP is, and by some of these definitions Auto is object oriented but by others it is not. In this chapter, we'll explore certain characteristics that are commonly considered object oriented and how those characteristics translate to idiomatic Auto.
Auto provides three key mechanisms for OOP-style patterns:
spec— specs define shared behavior (similar to Rust'strait)is— type composition and behavioral contracts (replacing inheritance)has— delegation of behavior through composition
We'll then show how to implement an object-oriented design pattern in Auto and discuss the trade-offs of doing so versus implementing a solution using some of Auto's strengths instead.
Characteristics of Object-Oriented Languages
There is no consensus in the programming community about what features a language must have to be considered object oriented. Auto is influenced by many programming paradigms, including OOP; for example, we explored the features that came from functional programming in Chapter 13. Arguably, OOP languages share certain common characteristics — namely, objects, encapsulation, and inheritance. Let's look at what each of those characteristics means and whether Auto supports it.
Objects Contain Data and Behavior
The book Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Addison-Wesley, 1994), colloquially referred to as The Gang of Four book, defines OOP in this way:
Object-oriented programs are made up of objects. An object packages both data and the procedures that operate on that data. The procedures are typically called methods or operations.
Using this definition, Auto is object oriented: types have data, and ext blocks provide methods on types. Even though types with methods aren't called objects in Auto, they provide the same functionality according to the Gang of Four's definition of objects.
The type keyword defines a data structure with fields, and ext blocks attach behavior. This is the Auto equivalent of a class in traditional OOP languages.
Encapsulation That Hides Implementation Details
Another aspect commonly associated with OOP is the idea of encapsulation, which means that the implementation details of an object aren't accessible to code using that object. Therefore, the only way to interact with an object is through its public API.
In Listing 18-1, the list and average fields are private by default in Auto. Only the methods marked pub are accessible from outside the type. The update_average method is private — it's an implementation detail.
We discussed how to control encapsulation in Chapter 7: we can use the pub keyword to decide which modules, types, functions, and methods in our code should be public, and by default everything else is private.
Because we've encapsulated the implementation details of AveragedCollection, we can easily change aspects, such as the data structure, in the future. For instance, we could use a Set<int> instead of a List<int> for the list field. As long as the signatures of the add, remove, and average public methods stayed the same, code using AveragedCollection wouldn't need to change.
If encapsulation is a required aspect for a language to be considered object oriented, then Auto meets that requirement. The option to use pub or not for different parts of code enables encapsulation of implementation details.
Inheritance: Replaced by Composition and Specs
Inheritance is a mechanism whereby an object can inherit elements from another object's definition, thus gaining the parent object's data and behavior without you having to define them again.
If a language must have inheritance to be object oriented, then Auto is not such a language. There is no way to define a type that inherits the parent type's fields and method implementations.
However, Auto provides powerful alternatives:
specwith default implementations — for code reuse (like Rust's traits)iscomposition — for type relationships (replacing inheritance)hasdelegation — for forwarding behavior to contained types
You would choose inheritance for two main reasons. One is for reuse of code: you can implement particular behavior for one type, and inheritance enables you to reuse that implementation for a different type. In Auto, you can achieve this with spec default implementations.
The other reason to use inheritance relates to the type system: to enable a child type to be used in the same places as the parent type. This is also called polymorphism. In Auto, you achieve this through spec bounds and generic types.
Polymorphism
Polymorphism means that you can substitute multiple objects for each other at runtime if they share certain characteristics.
Auto uses generics to abstract over different possible types and spec bounds to impose constraints on what those types must provide. This is sometimes called bounded parametric polymorphism, the same approach Rust takes.
The is/has/spec Triangle
Auto's approach to OOP centers on three keywords that work together:
| Keyword | Purpose | OOP Equivalent |
|---|---|---|
spec | Define shared behavior | Interface / Trait |
is | Compose types, assert contracts | Inheritance (type relationship) |
has | Delegate behavior via composition | Composition + delegation |
spec: Defining Shared Behavior
A spec defines a set of methods that a type must implement. This is identical to Rust's trait or an interface in Java/Go:
Types implement specs using the spec ... for Type syntax:
is: Type Composition and Contracts
The is keyword serves two purposes:
- Composing types — embedding one type's fields into another
- Asserting spec contracts — declaring that a type satisfies a spec
In Rust, composition requires accessing nested fields (dog.animal.name). In Auto, is flattens the composition — dog.name works directly because Dog is Animal.
You can also use is to assert that a type implements a spec:
type Dog {
is Animal
is Summary // Assert Dog implements Summary
breed String
}has: Delegation Through Composition
The has keyword delegates method implementations to a contained type:
In Rust, you must write delegation methods manually (self.engine.start()). Auto's has keyword automatically forwards method calls to the contained type.
is vs has
| Feature | is | has |
|---|---|---|
| Purpose | Compose fields | Delegate behavior |
| Field access | Flattened (direct) | Through the field |
| Method delegation | No | Yes |
| Multiple | A type can is multiple types | A type can has multiple types |
| Analog | Inheritance (data) | Composition (behavior) |
Use is when you want to share data structure. Use has when you want to share behavior without exposing the inner type's fields.
Using Specs for Polymorphism
Spec Objects: Dynamic Dispatch
Like Rust's dyn Trait, Auto supports dynamic dispatch through spec objects. When you need a collection of heterogeneous types that all implement the same spec, you use spec objects:
The dyn Draw syntax creates a spec object — a reference to any type that implements the Draw spec. This is exactly analogous to Rust's dyn Trait.
Implementing Specs for Concrete Types
Now a Screen can hold both Button and any other type that implements Draw:
Static Dispatch vs Dynamic Dispatch
When you use generics with spec bounds, Auto performs static dispatch — the compiler generates specialized code for each concrete type. This is the same as monomorphization in Rust:
// Static dispatch: each T gets its own compiled version
fn draw_all<T Draw>(items List<T>) {
for item in items {
item.draw()
}
}When you use spec objects (dyn Draw), Auto performs dynamic dispatch — the method to call is determined at runtime via a vtable:
// Dynamic dispatch: method looked up at runtime
fn draw_all(items List<Box<dyn Draw>>) {
for item in items {
item.draw()
}
}| Aspect | Static Dispatch (Generics) | Dynamic Dispatch (dyn) |
|---|---|---|
| Performance | Faster (can inline) | Slight overhead (vtable lookup) |
| Flexibility | Single type per call | Multiple types per call |
| Binary size | Larger (monomorphization) | Smaller |
| Use when | Type is uniform | Types are mixed |
The State Pattern in Auto
A classic OOP design pattern is the State pattern, where an object's behavior changes based on its internal state. Let's implement a blog post workflow that transitions through states: draft → review → published.
Implementing the State Pattern
Using the post:
Encoding State in the Type System
Auto's is keyword provides an alternative: encoding states directly into the type system. Instead of runtime state transitions, we use different types for each state:
This approach has a significant advantage: the type system prevents invalid operations at compile time. You can't call content() on a DraftPost — it doesn't exist on that type. You can't call approve() on a DraftPost — only request_review() is available. The compiler enforces the state transitions.
Usage:
fn main() {
let draft = DraftPost(content: "")
draft.add_text("I ate a salad for lunch today")
let review = draft.request_review()
let published = review.approve()
print(published.content())
}fn main() {
let mut draft = DraftPost { content: String::new() };
draft.add_text("I ate a salad for lunch today");
let review = draft.request_review();
let published = review.approve();
println!("{}", published.content());
}This is the idiomatic Auto approach: use the type system to make illegal states unrepresentable, rather than relying on runtime checks.
is/has/spec vs Rust's OOP
| OOP Concept | Rust | Auto |
|---|---|---|
| Interface / Trait | trait | spec |
| Implementation | impl Trait for Type | spec Trait for Type |
| Dynamic dispatch | dyn Trait | dyn Spec |
| Composition | Manual: self.inner.method() | has: automatic delegation |
| Field embedding | Manual: self.inner.field | is: flattened access |
| Default methods | trait with default body | spec with default body |
| No inheritance | No inheritance | No inheritance (use is/has) |
| State pattern | Box<dyn State> | dyn State or type-state |
Auto's is and has keywords remove boilerplate that Rust requires for composition and delegation. In Rust, composing two structs and delegating methods requires manual wrapper methods. In Auto, has generates those wrappers automatically.
Object-Oriented Patterns Quick Reference
| Feature | Auto | Rust |
|---|---|---|
| Define interface | spec Name { } | trait Name { } |
| Implement interface | spec Name for Type { } | impl Name for Type { } |
| Dynamic dispatch | dyn Spec | dyn Trait |
| Compose fields | is Type | Manual embedding |
| Delegate methods | has Type | Manual delegation |
| Default method | Body in spec | Body in trait |
| Encapsulation | pub / private by default | pub / private by default |
| Polymorphism | Generics + spec bounds | Generics + trait bounds |
Summary
Auto supports object-oriented patterns through a combination of features:
typeandext— packages data and behavior, like objects- Encapsulation —
pubcontrols visibility, fields are private by default spec— defines shared behavior, like interfaces or traitsis— composes types with flattened field accesshas— delegates behavior through automatic method forwardingdyn Spec— dynamic dispatch for heterogeneous collections- Type-state pattern — encode states in the type system for compile-time safety
Auto deliberately avoids inheritance in favor of composition (is/has) and behavioral contracts (spec). This produces more flexible designs where types only share the behavior they explicitly opt into, and the type system catches invalid state transitions at compile time rather than runtime.
In the next chapter, we'll explore Auto's pattern matching with the is keyword and how it compares to Rust's match expressions.