Skip to content

Instantly share code, notes, and snippets.

@Gisleburt
Created June 10, 2019 11:01
Show Gist options
  • Save Gisleburt/cb432d28aad488ff9c5231f9f24d2f83 to your computer and use it in GitHub Desktop.
Save Gisleburt/cb432d28aad488ff9c5231f9f24d2f83 to your computer and use it in GitHub Desktop.

How do you impl Display for Vec

This is a common question, and applies not only to Display and Vec, but how do you implement any trait from outside your crate for any type outside your crate?

Lets create a micro app that helps us explore the problem. We'll create a simple struct implement Display for that, then try to implement Display for a Vec of that struct.

Contents:

  1. The problem
    1. Preamble
    2. Display and vectors
  2. The solution
    1. The newtype pattern
    2. The ownership problem
  3. Improving the solution
    1. Referencing
    2. Dereferencing
  4. Conclusion

The problem:

Preamble

To begin, we need a simple struct to play with. Lets create a simple representation of a music album.

https://gist.github.com/c1a0cabca7e9bd09e6894650b2674a1c

Our app is going to print the albums in the format “title (artist)”. So, if we write:

https://gist.github.com/6758d2ece6d4292db27c7b204792235e

We want it to print:

https://gist.github.com/4a6812acc900c499396752b622bc0e88

To do this, we implement the std::fmt::Display trait for Album to format the output

https://gist.github.com/9e21226b5e5395dc1b8e27c737e6db09

And now when we run the program we get the expected result

https://gist.github.com/9d2dbec3cd0e0faebaf34acbc59e82a1

Code example preamble.rs
Run the example cargo run --bin preamble

Display and Vectors

Now to tackle the problem at hand. Lets try to implement Display for a Vec of Albums. Lets start by creating the Vec and handing it to println!

https://gist.github.com/18452f1a46ee86afaa961815d3a76de7

Obviously this won't work because we haven't described how the Vec should be displayed, but lets do some Compiler Driven Development and get an idea of whats going wrong

https://gist.github.com/396203ff8ed372c8dcccace712dfcab9

As usual the compiler does it's best to tell us whats wrong, and as we expected, it won't compile until we implement Display for Vec

https://gist.github.com/777acbb16e32a587c9b9af8579752a59

So let’s try that:

https://gist.github.com/e36b589bd3a0dc75598377b4dc52c8b1

ℹ️ A word on what this function is doing. For each item in the Vec, we want to write a new line to the formatter, however write! and writeln! return a fmt::Result, and we also need to return a fmt::Result. Using a fold allows us to do that multiple times and return a single result for the whole set. The and_then, means we won’t try to write any more after the first error, and will return that same error from our function.

Lets try again:

https://gist.github.com/e2ff74ea23a3adcc04f89ecd39d776a8

Now we have a new error, but if we look closely the compiler has told us why this doesn't work.

https://gist.github.com/21eb776d5c6b6b07a9ee132e8c70717a

In Rust you may implement traits from your crate onto types from other crates, or you may implent traits from other crates onto your types.

⚠️ You can not apply external traits onto external types.

Since Display and Vec are both in the standard library, neither is in our crate, we may not implement one for the other.

But, we can get around that.

Code example display-and-vectors.rs
Run the example cargo run --bin display-and-vectors

The solution:

The newtype pattern

The solution to our problem is actually mentioned in the next line of the error:

https://gist.github.com/6ae585f89aaddd44886da8ebcd4ceef9

We can’t define a new trait since we need to use Display, but we can define a new type. Obviously we don’t want to create our own Vec, but we can instead use the newtype pattern.

This pattern simply wraps one type in another. In our case:

https://gist.github.com/b7ebc2db48541e806bc3d2038be746bb

ℹ️ This pattern is normally used to improve type safety. For example if your program needs to deal with emails which are stored in Strings, you may want a function that only handles Emails and not any old String. You could use newtype idiom to enforce this:

struct Email(pub String);

There’s one little quirk with the newtype pattern though which is that because we wrapped the data in another type, if we want to access the data itself, we need to access the zeroth element of our wrapper. I.e, if we're accessing a Vec called albums, we can just use albums. If the variable contains our newtype, to access the Vec inside we must use albums.0.

If we change our implementation of fmt::Display to use our newtype, it looks like this.

https://gist.github.com/ec443278c597623b0fef5c9d55dfacd1

We also need to wrap our Vec in our Albums newtype before we can print it.

https://gist.github.com/3b6f01504c15e4e2ffca6cef3f9ae103

Now our program outputs exactly what we wanted.

https://gist.github.com/d24e10ddcbac527d8e08ebbb669fab8d

There’s still a problem though, lets dig a little further.

Code example [newtype.rs]
Run the example cargo run --bin newtype
Further Reading Rust by example: New Type Idiom

The Ownership Problem

Lets contrive a more complex example. What if the albums belong to another struct. What type should we use here?

https://gist.github.com/d5f4f708a84ae646c200681b66eed751

The obvious choice, and usually the right one, is to use Albums, but that might not work in every use case.

If we’re only using the newtype for Display it will add some mental overhead where we want to use the Vec underneath, so lets give our User a Vec of Album and look at how we can get our Albums type for Displaying it.

https://gist.github.com/24e27a29d498622e696de0323f418fe2

Uh oh! The problem here is that User owns the Vec<Album> data that we need for our newtype, to get at it we only have two options:

  1. We consume User and return just its albums
  2. We make a copy of the data in albums (note: we can derive Clone for Album to do this)

Neither of these are particularly desirable, is there a better way?

Code example ownership-problem.rs
Run the example cargo run --bin ownership-problem

Improving the solution

Referencing:

What if, rather than taking ownership of the data, the newtype just took a reference to the data? We can do that, it's going to get a little rocky but it'll be worth it:

https://gist.github.com/fad84ceb5fa8882ccc1adc12ce62f397

“Argh! Lifetimes!” I hear you cry. (Or is it just me?)

Don’t worry though, all this says is that Albums has a lifetime ('a), which is tied to the owned value that &Vec<Album> references. I.e. The compiler will check that Albums isn't used after the owned Vec<Album> has been discarded.

This does change our implementation a little because we now need to acknowledge the lifetime, but it’s not involved in the display itself so only the first line has to change.

https://gist.github.com/9fc00a6d35941b04a1f4d1edd2df41c4

We can now wrap the album data without having to take ownership of it:

https://gist.github.com/19d444871107e608e2166327613fbcbf

Where did the lifetimes go? Not that long ago, you would have had to specify the lifetimes on this function too, but today Rust is smart enough to know if one reference is going in (&self), and one is coming out (&self.albums) they must have the same lifetime.

Our code now works as you’d expect without making any unnecessary memory allocations, or consuming data we may want to use later.

https://gist.github.com/d06f82cbd4b450b186050b8d8bcb47ea

Code example referencing.rs
Run the example cargo run --bin referencing

Dereferencing

There is one more little trick you can use to make your code even cleaner. Let’s go back to when we said the User.albums probably should be the Albums newtype, is there anything we can do to make using it easier?

For example, we don’t want to type daniel.albums.0 every time we want access to the underlying vector. We shouldn’t have to understand the implementation of the object in order to use it.

Well, there’s a trait for that, std::ops::Deref. Going back to our original type that owned its data:

https://gist.github.com/82204ef84dbfc18d00b48e0d9eaa4769

We can implement the Deref trait to allow the outer type to be treated as though it is a reference to the inner type. We implement Deref for Album like this:

https://gist.github.com/a9d8f791538bb9cfe07c0abd3eaeeb6c

We can take immediate advantage of this in our Display implementation:

https://gist.github.com/b6e5dbbc1308c998586ca687c64157c5

Notice, we no longer need the .0 when getting an iterator. The iter function only needs a reference to the vector, it does not need ownership of it, so this works well.

Our User object now needs the newtype wrapper to go back in, but we can now treat albums as both a Albums type and a &Vec<Album> type.

https://gist.github.com/0f6875506f2b71c813def79024090e83

You can remove the impl User code entirely.

Code example dereferencing.rs
Run the example cargo run --bin dereferencing

Conclusion

Here's what we've learned:

  1. You can not apply external traits onto external types.
  2. You can use the newtype idiom to wrap types, making it yours, allowing you to apply the external traits
  3. You can use use references inside of newtypes
  4. You can use the Deref trait to expose the contents of the newtype
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment