Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When should I use tuple structs over normal tuples?

Tags:

types

rust

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)?

like image 971
Chakravarthy Raghunandan Avatar asked Oct 16 '16 11:10

Chakravarthy Raghunandan


2 Answers

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 u32s, it would take LineNumbers -- 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!

like image 109
Lukas Kalbertodt Avatar answered Nov 07 '22 16:11

Lukas Kalbertodt


Tuple structures are less common. Use them when you only have a few members and it's clear enough which are which that they don't need a name.

One common use of tuple structures is newtypes. This is a tuple structure with only one member. This is useful to make simple wrappers around existing types.

like image 44
mcarton Avatar answered Nov 07 '22 16:11

mcarton