Generic Types, Specs, and Lifetimes
Every programming language has tools for effectively handling the duplication of concepts. In Auto, one such tool is generics: abstract stand-ins for concrete types or other properties. We can express the behavior of generics or how they relate to other generics without knowing what will be in their place when compiling and running the code.
Functions can take parameters of some generic type, instead of a concrete type like int or String, in the same way they take parameters with unknown values to run the same code on multiple concrete values. In fact, we already used generics in earlier chapters: ?T (Option), !T (Result), List<T>, and Map<K, V> are all generic types.
In this chapter, we'll explore how to define your own types, functions, and methods with generics. Then we'll learn about specs (Auto's equivalent of Rust traits) for defining shared behavior. Finally, we'll briefly discuss lifetimes — and how Auto's compiler handles them automatically, eliminating the need for explicit lifetime annotations.
Generic Data Types
In Function Definitions
When defining a function that uses generics, we place the generics in the signature where we would usually specify the data types. This makes our code more flexible while preventing duplication.
We read fn largest<T>(list []T) T as "the function largest is generic over some type T." The function takes a list of T values and returns a T. By convention, type parameter names are short, often just one letter, using UpperCamelCase — T (short for "type") is the default choice.
The syntax is the same as Rust's: type parameters go inside angle brackets <> between the function name and the parameter list.
In Type Definitions
We can define types with generic type parameters in their fields:
Because we've used only one generic type T, both x and y must be the same type. If you need different types for each field, use multiple type parameters:
In Method Definitions (ext)
We can define methods on generic types using ext:
Note that we declare T after ext and before Point<T>, just like Rust's impl<T> Point<T>. This tells the compiler that the method is defined for any concrete type that T might be.
Performance of Code Using Generics
Using generics has zero runtime cost. Auto (like Rust) uses monomorphization: the compiler generates specific code for each concrete type used with a generic. So Point<int> and Point<f64> become separate, optimized types at compile time. There is no runtime overhead for using generics.
Defining Shared Behavior with Specs
A spec (short for "specification") defines the functionality a particular type has and can share with other types. Specs are Auto's equivalent of Rust's traits (also known as interfaces in other languages).
Defining a Spec
A type's behavior consists of the methods we can call on that type. Spec definitions group method signatures together to define a set of behaviors:
spec Summary {
fn summarize(self) String
}This declares a spec named Summary with one required method: summarize, which takes self and returns a String. Any type that implements this spec must provide its own summarize method.
Implementing a Spec on a Type
In Auto, you implement a spec using as:
Key differences from Rust:
| Feature | Auto | Rust |
|---|---|---|
| Define spec/trait | spec Summary { ... } | trait Summary { ... } |
| Implement on type | type Foo as Summary { ... } | impl Summary for Foo { ... } |
| Method body | In separate ext block | Inside impl block |
The as keyword on the type declaration means "this type conforms to the spec." The actual method implementations go in the ext block.
Using Specs as Parameters
You can use a spec as a parameter type to accept any type that implements it:
In Auto, fn notify(item Summary) accepts any value whose type implements the Summary spec. This is equivalent to Rust's impl Trait syntax: fn notify(item: impl Summary).
Note how Auto's syntax is simpler — you don't need impl or Box<dyn Trait> to pass spec-conforming values. The compiler handles the dispatch automatically.
Generic Specs
Specs can have their own type parameters, just like types:
The generic spec Container<T> defines a contract that any implementing type must provide get and set methods for its inner type T. The Box<T> type implements Container<T>, and the ext block provides the method bodies.
Spec vs Trait Quick Reference
Auto Rust Meaning spec Foo { ... }trait Foo { ... }Define a specification type Bar as Foo { ... }impl Foo for Bar { ... }Implement spec on type fn foo(x SomeSpec)fn foo(x: impl SomeSpec)Spec as parameter spec Container<T>trait Container<T>Generic spec ext Type<T> { ... }impl<T> Type<T> { ... }Generic method extension
Lifetimes: Automatic in Auto
In Rust, lifetimes are generic annotations that tell the compiler how long references should be valid. They're necessary because Rust needs explicit information about the relationships between references.
Auto takes a different approach: lifetimes are fully automatic. The compiler tracks reference validity internally and ensures memory safety without requiring you to write any lifetime annotations.
No Lifetime Annotations Needed
In Rust, a function that returns the longer of two string slices requires explicit lifetime annotations:
// Rust requires explicit lifetimes
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}In Auto, the same function needs no annotations at all:
Auto's compiler uses ownership and borrowing rules (.view, .mut, .move) to track reference validity at compile time, eliminating the need for explicit lifetime parameters in most cases.
What This Means for You
- No
'aannotations in function signatures - No lifetime parameters on struct definitions
- No
<'a>declarations inextblocks - The compiler still prevents dangling references and use-after-free bugs
If you're coming from Rust, you can think of it this way: Auto's compiler applies Rust's lifetime elision rules aggressively, plus additional inference, so you almost never need to write lifetimes explicitly.
Summary
Auto provides the same generic programming capabilities as Rust, with a cleaner syntax:
| Concept | Auto | Rust |
|---|---|---|
| Generic function | fn foo<T>(x T) | fn foo<T>(x: T) |
| Generic type | type Point<T> { x T } | struct Point<T> { x: T } |
| Multiple type params | type Pair<T, U> | struct Pair<T, U> |
| Generic methods | ext Point<T> { ... } | impl<T> Point<T> { ... } |
| Spec (trait) | spec Foo { ... } | trait Foo { ... } |
| Implement spec | type Bar as Foo { ... } | impl Foo for Bar { ... } |
| Spec as parameter | fn foo(x SomeSpec) | fn foo(x: impl SomeSpec) |
| Generic spec | spec Storage<T> | trait Storage<T> |
| Lifetimes | Automatic | Explicit 'a annotations |
The most notable simplification is Auto's automatic lifetime inference. Where Rust requires <'a> annotations on function signatures and struct definitions, Auto's compiler handles this internally, letting you focus on your logic rather than lifetime management.
In the next chapter, we'll discuss automated testing.