Holy std::borrow::Cow!
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 theFrom
trait to convert ourString
to aCow<'a, str>
for any lifetime. Type inference will do the dirty work for us, all we need is tomap(From::from)
, and we’re golden. -
We got a
SpanSnippetError
. In this case, we want to return the default wrapped in aCow<'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 ourCow
out of the result, we useunwrap_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…