Tuples are collections of any number of values of different types. They are sometimes useful when functions have to return multiple results.
In this article, we’re going to dive into tuples in Rust and, in particular, into the differences with respect to Rust structs.
ToC:
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
Rust’s documentation defines tuples as a “finite heterogeneous sequence” of values:
We can create a tuple simply by writing a comma-separated list of values inside parentheses, optionally specifying the types:
fn main() {
let immutable: (String, u8) = (String::from("LogRocket"), 120);
println!("{} {}", immutable.0, immutable.1);
}
In the example above, we created a simple tuple called immutable, where the first value is a String and the second an unsigned integer with 8 bits. Then, we printed the elements of the tuple by accessing them by index, starting from 0. When run, the example prints “LogRocket 120”.
Tuples are, by default, immutable. For example, if we tried to assign a new value to one of the elements of the tuple above, we’d get an error:
fn main() {
let immutable: (String, u8) = (String::from("LogRocket"), 120);
immutable.1 = 142; // cannot assign
}
To define the tuple as mutable, we have to specify mut at declaration time:
fn main() {
let mut mutable = (String::from("LogRocket"), 120);
mutable.1 = 142;
println!("{} {}", mutable.0, mutable.1);
}
In either case, we can destruct a tuple to initialize other variables:
fn main() {
let mut mutable = (String::from("LogRocket"), 120);
let (name, value) = mutable;
println!("{name}");
}
The program above will just print “LogRocket”.
Lastly, we can assign an alias to a tuple type using type:
type Test = (String, u8);
fn main() {
let immutable: Test = (String::from("LogRocket"), 120);
println!("{} {}", immutable.0, immutable.1);
}
This makes the code more readable. It is important to note, however, that what we defined with type is just an alias. Test above and (String, u8) are structurally equivalent.
In this section, we’re going to introduce two alternatives to tuples, namely structs and tuples structs. If tuples are anonymous types with anonymous fields, structs are named types with named fields. Tuple structs are in the middle of the spectrum, as they provide us with named types with anonymous fields.
structs are similar to tuples in that they can handle multiple values of different types. The main difference is that the elements of a struct all have a name. Hence, instead of accessing them by index, we access them by name:
struct Person {
name: String,
age: u8
}
fn main() {
let p = Person {
name: String::from("Jhon Doe"),
age: 21
};
println!("Name: {}, age: {}", p.name, p.age);
}
Generally speaking, the ability to name the elements of a struct makes the latter more flexible. Hence, for the sake of readability, it is often preferable to use a struct rather than a tuple. Nonetheless, this is not always true, as we’ll see in the next section.
Tuple structs are the same as structs, with the only difference being that we don’t have to name their fields:
struct Wrapper(u8);
fn main() {
let w = Wrapper{0: 10};
println!("{}", w.0);
}
Since the fields don’t have a name, we must access them by index. In the example above, we define a struct to wrap a u8. Then, to read and write it, we have to refer to it using its index, 0.
structs, and tuple structsThis section compares tuples, structs, and tuple structs. First, we’ll look at a few key low-level differences. Then, we’ll see practical use cases for each of them.
Since tuple structs are just structs with no name for the fields, in this section, we’ll mainly compare tuples with tuple structs. What we’ll see for tuple structs is true for structs as well. As a matter of fact, tuple structs desugar into regular structs.
First, the elements of a tuple struct are private by default, and cannot be accessed outside the module they’re defined in. Additionally, tuple structs define a type. Hence, two tuple structs with fields of the same type are two different types.
On the other hand, two tuples whose elements are of the same type define a single type. In other words, tuples are structurally equivalent, whereas tuple structs are not.
Secondly, we can add attributes to tuple structs, such as #[must_use] (to issue a warning when a value is not used) or #[derive(...)] (to automatically implement traits in the struct). Tuples, on the other hand, cannot have attributes but do implement a few traits by default.
Thirdly, tuple structs implement the Fn* family by default, allowing them to be invoked like functions or passed in as parameters to higher-order functions:
struct Name(String);
fn main() {
let strings = [String::from("Log"), String::from("Rocket")];
let names = strings.map(Name);
}
Furthermore, tuple structs support the struct update syntax, simplifying the way we can create a new instance of a struct where most of the values are equal to another instance of the same struct:
struct Person(String, u8);
fn main() {
let john = Person {
0: String::from("John"),
1: 32
};
let another_john = Person {
1: 25,
..john
};
println!("name: {}, age: {}", another_john.0, another_john.1);
}
In the example above, we first created an instance of Person, john, and then used it to create a new one, another_john, editing only a subset of the fields defined by Person.
Lastly, we can define methods on tuple structs, whereas we cannot on tuples:
struct Person(String, u8);
impl Person {
fn is_adult(&self) -> bool {
return &self.1 >= &18;
}
}
fn main() {
let p = Person {
0: String::from("John"),
1: 20
};
println!("{}", p.is_adult());
}
As a rule of thumb, we should use tuple structs as soon as the name of the type carries semantic information. Then, we should move to structs when there’s more than one field.
Nonetheless, in some cases, it is just more readable not to have names. For example, the as_chunks method of slice is defined as follows:
pub fn as_chunks<const N: usize>(&self) -> (&[[T; N]], &[T])
It basically inputs a constant N, defining the size of the chunk, and returns a tuple where the first element is an array of chunks of size N, whereas the second element is the remainder of the array (that is, the last values that were not enough to compose a new chunk).
In this case, the types themselves make it clear what each element of the result represents. Hence, having names would likely be overkilling. A tuple is a good solution in this case.
Nonetheless, we normally work with complex data types, where having names helps us read the code. For example, consider a program that manipulates book information, in particular the title, the author, and the price. If we were to model it with a tuple, we might come up with:
type Book = (String, String, f32)
Our definition of Book, however, contains two String fields. How can we know which of those represents the title and which the author? In this case, a struct is a much better choice:
struct Book {
title: String,
author: String,
price: f32
}
Still, sometimes we may find ourselves working with structs with only one element. We may want to do so, for instance, to use types to further enhance the clarity of our code. Consider a function to build a URL out of three components:
At first, we might come up with a function with the following signature:
fn make_url(
subdomain: String,
domain_name: String,
top_level_domain: String
) -> String {
todo!();
}
fn main() {
make_url(
String::from("www"),
String::from("mydomain"),
String::from("com")
);
}
We still have lots of Strings in a single structure here, though, and it’s very easy to confuse the ordering of the parameters. Furthermore, Rust doesn’t support named parameters, so we have to remember the correct ordering.
Using tuple structs, we can write a more self-explanatory signature:
struct Subdomain(String);
struct DomainName(String);
struct TopLevelDomain(String);
struct URL(String);
fn make_url(
subdomain: Subdomain,
domain_name: DomainName,
top_level_domain: TopLevelDomain
) -> URL {
todo!();
}
fn main() {
make_url(
Subdomain(String::from("www")),
DomainName(String::from("mydomain")),
TopLevelDomain(String::from("com"))
);
}
In the example above, we leveraged tuple structs to wrap Strings. In this case, it’d make no sense to name the field of each struct. In fact, it’d make the code more complex to read.
In this article we dove into tuples in Rust. First, we briefly saw what they are and how to use them. Secondly, we briefly introduced possible alternatives, namely structs and tuple structs. Lastly, we compared all those alternatives by looking at low-level implementation details as well as practical use cases for each of them.
In the end, they can be used to fulfill more all less the same use cases. What really matters in the choice is, as usual, how they impact the readability and maintainability of the code.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket lets you replay user sessions, eliminating guesswork around why bugs happen by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks.
LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.
Modernize how you debug your Rust apps — start monitoring for free.

:has(), with examplesThe CSS :has() pseudo-class is a powerful new feature that lets you style parents, siblings, and more – writing cleaner, more dynamic CSS with less JavaScript.

Kombai AI converts Figma designs into clean, responsive frontend code. It helps developers build production-ready UIs faster while keeping design accuracy and code quality intact.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 22nd issue.

John Reilly discusses how software development has been changed by the innovations of AI: both the positives and the negatives.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now