When should I use tuple structs over normal tuples?
rust, types
Solution
Summary
Use tuple structs when:
- the name of the type carries semantic information and
- naming the fields does not add any semantic information
You will find that this is not too often the case. If it is, you are usually just trying to wrap exactly one other type into a new type to give it different behavior. This is a known pattern, called 'newtype' pattern. When you have multiple fields in your struct, you usually want to name them.
Examples
As a quick reminder:
- tuples are anonymous types with anonymous fields
- tuple structs are named types with anonymous fields
- structs are named types with named fields
Let's take a look at `[T]::split_at()`:
fn split_at(&self, mid: usize) -> (&[T], &[T])
They authors of this function choose to return a tuple here. Let's see, would we gain anything from...
- naming the type: not really, right? What would we call it? `SplitSlice`, `SliceParts`, ...? Every name we could give is superfluous, because the function is already named appropriately.
- naming the fields: questionable. `left` and `right` would make it more clear what part is what side. But here we assume that programmers have the correct intuition. English is written from left to right, in English cultures arrays are usually drawn from left to right (`[0 | 1 | 2 | 3 ]`), so it kind of makes sense for most people.
⇒ just a tuple is fine!
Another example: imagine you are writing an application that somehow works with a text file (compiler, text editor, ...). When you are talking about a region in the text file (for example, a search result), you want to specify that region by giving byte offsets. Let's see if tuples work out for us:
fn find_first_occurence(file: &TextFile, needle: &str) -> (usize, usize)
Does the return type speak for itself? Rather not... even if you know that regions in the file are specified by byte offsets, the return value is still ambiguous: either it's `(start, end)` or it's `(start, stuff)`, where stuff can be any other search metric (the function doesn't need to return `end`, since we already know the length of `needle` and thus could calculate it). So, I hope you agree, we want to name the return type. Let's call it `Span` -- that's the name used in the Rust compiler.
Next question: struct or tuple struct? Does it make sense to name the fields? Again, there is no clear answer, but I'd argue that we do want to name the fields. What is easier to read: `span.1 - span.0` or `span.high - span.low`? Additionally, we can write documentation for the named fields; for example, to document that `high` is exclusive.
⇒ struct
Now imagine you want to report line numbers to the user. Especially important is a function that returns the corresponding line number for a given span (for simplicity's sake, we assume this span never spans more than one line).
fn get_line_number(file: &TextFile, span: Span) -> ???
So what do we return? In the context of this function a simple `u32` is probably fine! There is no doubt what this `u32` represents. Although: does the line number counting starts with 0 or 1? sigh
Sure, we could document this property on the function ... and every other function working with line numbers. So how about creating a new type and document it there? This will also help with functions taking multiple numbers, including line numbers:
print_snippet(&file, 57, 63, 80);
Wait, what are line numbers now? Exactly: instead of taking `u32`s, it would take `LineNumber`s -- the type system is the documentation.
We now agreed on creating a new type. But: struct or tuple struct? Let's try struct:
struct LineNumber {
line_number: u32, // uhm...
}
Well, how to call the field? The only fitting names fall into two categories:
- like the struct name: `line_number`, `number`, `line`, ...
- without semantic information: `inner`, `value`, `data`, ...
There is not really a benefit in naming the field. So let's don't and use a ...
⇒ tuple struct
The decision to make it an own type has some nice consequences: we can use 0-based numbers in our source code but never need to worry about printing it incorrectly:
impl fmt::Display for LineNumber {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
(self.0 + 1).fmt(f)
}
}
We can adjust the 0-based number to a 1-based number (for those damn humanz!) in a single place!
Also note that we were talking about byte offsets above. Instead of using `usize`, we should also create a new type to distinguish it from char offsets, for example!
Problem
In The Rust Programming Language book, in the Structs chapter we are introduced to tuple structs. Under which cases should I use tuple structs over normal tuples (other than the example mentioned in the book)?