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 struct
s.
ToC:
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 struct
s and tuples struct
s. If tuples are anonymous types with anonymous fields, struct
s are named types with named fields. Tuple struct
s are in the middle of the spectrum, as they provide us with named types with anonymous fields.
struct
s 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 struct
s are the same as struct
s, 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
.
struct
s, and tuple struct
sThis section compares tuples, struct
s, and tuple struct
s. First, we’ll look at a few key low-level differences. Then, we’ll see practical use cases for each of them.
Since tuple struct
s are just struct
s with no name for the fields, in this section, we’ll mainly compare tuples with tuple struct
s. What we’ll see for tuple struct
s is true for struct
s as well. As a matter of fact, tuple struct
s desugar into regular struct
s.
First, the elements of a tuple struct
are private by default, and cannot be accessed outside the module they’re defined in. Additionally, tuple struct
s define a type. Hence, two tuple struct
s 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 struct
s are not.
Secondly, we can add attributes to tuple struct
s, 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 struct
s 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 struct
s 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 struct
s, 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 struct
s as soon as the name of the type carries semantic information. Then, we should move to struct
s 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 struct
s 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 String
s 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 struct
s, 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 struct
s to wrap String
s. 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 struct
s and tuple struct
s. 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 is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
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 nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.