Holy std::borrow::Cow!

09 July 2015

Recently I got into a reddit discussion about how lifetimes are in fact quite hard. Not having had to deal with lifetimes and ownership so far within clippy, because just about everything we use is already owned by the compiler, I did not have much of an opinion about lifetimes, when user The_Doculope stated:

This is where lifetime elision bites us in the ass.

(/u/The_Doculope on /r/rust)

Needless to say I was shocked!

And here I am, thankful lifetime elision freed me from worrying to much, only to learn that it’s out to bite me in the ass. Now I’m becoming paranoid. ;-P

(me on /r/rust)

Right after writing this, I happened upon a source line in clippy I wasn’t quite happy with, because it cloned a string (with .to_string() ! blasphemy!) just to use it in a &format(…) . Now this was in a Result<String, …>::unpack_or(…) , so I first tried to wrangle the lifetimes of an owned string against a given string, to no avail.

Alas, it was all for naught. I could not give an owned string the same lifetime as a string borrowed from the compiler, which promptly told me so in not quite uncertain terms.

I will spare you the gory error messages, as I’m sure if you have written code like this in Rust, you will already know them, and otherwise, well, it’s not so bad anyway, because luckily I remembered that handy std::borrow::Cow , and rewriting the snippet with it (and std::convert::From ) was a piece of cake. I even created a fn to make it reusable, thus I now have officially written my first lifetime annotation on a method. [ACHIEVEMENT UNLOCKED]

What does a Cow do (besides saying Moo)? Cow is an abbreviation for Clone-on-write (usually Copy on Write, but copy has a different meaning in Rust), a cool idea that means we can use both an owned instance of something or a borrowed instance using the same code.

For the interested, the function is:

pub fn snippet < 'a > ( cx : & Context , span : Span , default : & 'a str ) -> Cow < 'a , str > { cx .sess () .codemap () .span_to_snippet ( span ) .map ( From :: from ) .unwrap_or ( Cow :: Borrowed ( default )) }

Bonus: Perhaps an explanation of the function is in order: The cx.sess().codemap() fetches the CodeMap from our lint session. This map has functions to get the source of a Span (which most AST elements have). Notably, the span_to_snippet function takes a span and returns a Result<String, SpanSnippetError> (the error is returned for ill-formed spans, or spans which simply have no available source).

Now we have two cases:

Our Result .is_ok() . We can use the From trait to convert our String to a Cow<'a, str> for any lifetime. Type inference will do the dirty work for us, all we need is to map(From::from) , and we’re golden. We got a SpanSnippetError . In this case, we want to return the default wrapped in a Cow<'a, str> where 'a is the lifetime of the given default argument. This is why I wrote the function generic over the lifetime of the default argument: We can use its lifetime for our Cow. To handle this case while getting our Cow out of the result, we use unwrap_or .

The Result type gives us the neat methods to deal with it in a fairly readable way. As Cow<'a, str> Deref s to str , we can directly use it in format! . Should we want to change the string, we could have used Cow’s into_owned() function.

The possibly messy handling of lifetimes is reduced to one generic lifetime for the function plus two annotations for the argument and result. Also, I can use the function everywhere in my code without needing to worry over the lifetimes.

So lifetime elision may still bite me in the ass some day. Today was not that day.

Continue reading at the Redux…