Iterators yielding mutable references
24 October 2013
There is a known bug with the borrowck rules that causes it to
be overly permissive. The fix is relatively simple but it
unfortunately affects some of our Iterator
implementations,
specifically those iterators that iterate over &mut
values. The
short version is that while it is possible to expose a safe
interface for iterating over &mut
values, it is not possible to
implement such iterators without an unsafe block.
After giving this quite a bit of thought, I have come to the conclusion that we have three options:
Keep things as they are, but accept that some iterators over mutable references will require unsafe implementations.
Split the
Iterator
trait intoIterator
andMutIterator
. The latter would only be used for iterating over mutable references.Extend the type-system with higher-order types or integrate theorem provers to prove much higher-level constraints than we can currently reason about. I don’t really consider this to be an option at this point in time, but I will briefly describe the sorts of extensions that might address the problem.
In this post, I’ll explain the problem, describe the possible solutions, and finally dive into some of the longer term implications.
What is the problem?
The iterator trait
To explain the problem, let’s begin by examining the basic iterator trait:
pub trait Iterator<A> {
fn next<'n>(&'n mut self) -> Option<A>;
}
next()
will be mutating the iterator itself to update the current
position and so forth, so it takes an &mut self
parameter. I’ve
opted to make the lifetime 'n
of this pointer explicit, even though
it’s not yet necessary, because this lifetime will feature heavily in
the discussion to come.
You can see that the iterator trait is parameterized by a type A
that indicates the kind of values being iterated over. One of the
appealing aspects of the iterator trait is that it is able to
encompass both by value iteration (when the type A
is something like
int
) and by reference iteration (when the type A
is something like
&T
). This all works great, the problems arise when we extend this
same iterator trait to handling mutable references like &mut int
. But let’s take it step by step.
An iterator over mutable references
Now, let’s examine how we might implement a mutable iterator over a slice. By “mutable iterator” I mean an iterator that iterates over mutable references into the slice, and thus permits you to modify the contents of the slice in place. For example, one might use a mutable iterator to increment all the elements in a vector like so:
let mut vec = ~[1, 2, 3];
for ref in vec.mut_iter() {
*ref += 1;
}
// vec now equals ~[2, 3, 4];
What follows is a simple implementation of a mutable vector. For the
moment, I have omitted the body of the next()
method, so you just
see the struct
declaration and the impl
of the Iterator
trait. The interface below looks reasonable, but we will see that
this interface as specified below is unsafe, and would permit user
code to create segfaults. We will also see that (as you would expect,
since it can cause segfaults) there is no way to implement this
interface without an unsafe block (modulo #8624).
// 'v: lifetime of the vector
struct VecMutIterator<'v, T> {
data: &'v mut [T],
index: uint,
}
impl<'v, T> Iterator<&'v mut T> for VecMutIterator<'v, T> {
// 'n: lifetime of the call to next()
fn next<'n>(&'n mut self) -> Option<&'v mut T> {
// Note lifetime of the result: ^~~
....
}
}
A mutable iterator holds both a mutable slice (the field data
) and
an index into that slice (index
). On every call to next()
, it
returns another pointer into that slice. What is crucial in this bit
of code is the lifetime of the pointers that get returned from
next()
. You can see that the lifetime is 'v
, which represents the
lifetime of the slice (I have highlighted the relevant bit with a
comment). Using the lifetime 'v
for those returned pointers makes
some measure of sense. After all, the pointers are pointers into the
slice self.data
, and self.data
has the lifetime 'v
.
The danger arises because of Rust’s rule that mutable references must
be unique. In a nutshell, Rust requires that every &mut T
pointer
must be the only way to mutate the memory it references. This rule
ensures memory safety and can also serve other purposes, such
as preventing data races (I have in the past waxed
philosophical about how
those two classes of errors are two sides of the same coin).
The borrow checker is the much loved (or sometimes hated) bit of code tasked with enforcing this rule. To see an example of how the rules work, consider this erroneous snippet of code:
// vec is an array of boxed integers.
let mut vec: ~[~int] = ~[~1, ~2, ~3];
// Using an iterator, we create a pointer
// `ptr0` that points to the first box in the list,
// and then a pointer `int0` that points directly
// at the integer in that box.
let mut iterator = VecMutIterator { data: vec, index: 0 };
let ptr0: &mut ~int = iterator.next().get();
let int0: &mut int = &mut **ptr0;
// Now, we modify the vector so as to replace
// the first box. This will cause the original box
// to be freed, and would make `int0` a DANGLING POINTER.
vec[0] = ~4; // ERROR
// Accessing `int0` now could cause a crash:
let i = *int0;
Here the user has created an iterator and started iterating over the
vector, but then they attempt to mutate the vector and replace its
first element. The borrow checker will flag this as an
error. Intuitively, what happens is that the capability to mutate the
vector is taken from vec
and moved into the iterator. Once the
iterator goes out of scope, the capability will return to vec, but in
the meantime code like vec[0] = ~4
is illegal.
However, a devious user might note that there is another way to
create a crash. When creating the iterator, we gave up the capability
to access vec
, but nowhere did we give up the capability to
access the iterator itself. That means that someone could write:
// Same as before:
let mut vec: ~[~int] = ~[~1, ~2, ~3];
let mut iterator = VecMutIterator { data: vec, index: 0 };
let ptr0: &mut ~int = iterator.next().get();
let int0: &mut int = &mut **ptr0;
// Same EFFECT as before, but expressed differently:
iterator.data[0] = ~4;
Thus we see that the VecMutIterator
type/impl I showed before is
broken. Fundamentally, the problem is that the code did nothing to
ensure that the mutable references returned are unique.
What’s interesting is that the iterator protocol itself is fine. That
is, so long as we only obtain pointers by invoking next()
repeatedly, we will always get a new pointer each time, and hence
there is no overlap between the returned values. But because the slice
in its entirety is still available, things break down (astute readers
may note that this hints at a possible solution, see below).
How would the Rust type system prevent this?
Given that the VecMutIterator
type/impl I showed before can be used
to create crashes, one would hope then that the Rust type system would
prevent you from using such an interface, and in fact it does (or
will, once I push the fix for #8624). More specifically, there
is no way to implement the body of the next()
method without using
an unsafe
block.
To see how the type system rule works, let’s examine a possible
implementation of the Iterator
impl for VecMutIterator
:
impl<'v, T> Iterator<&'v mut T> for VecMutIterator<'v, T> {
// 'n: lifetime of the call to next()
fn next<'n>(&'n mut self) -> Option<&'v mut T> {
let index = self.index;
self.index += 1;
if index < self.length {
let ptr: &'v mut T = &mut self.data[index]; // ERROR
Some(ptr)
} else {
None
}
}
}
The code is straight-forward. We save the current index, increment the
index field for next time, and then return a pointer into self.data
at the saved index.
The type check error occurs when we attempt to create the pointer
ptr
. The compiler flags this line as erroneous because it cannot
guarantee that ptr
will be unique for its entire lifetime. Here the
lifetime of the pointer is 'v
, which means that the compiler must be
able to guarantee that for the entirety of 'v
nobody will be able to
mutate the source of the pointer (self.data[index]
). The only means
that the compiler has of making this guarantee is to prevent access to
self.data
. The problem is that the lifetime of self
is only 'n
:
that is, the duration of the call to next()
. That means that if we
prevent you from accessing self
again, we could be sure that ptr
would be unique for the lifetime 'n
, but not the entire lifetime
'v
, which might be longer than 'n
. Therefore an error is reported.
In order to make this impl type check, the next()
function
would need to return pointers with the lifetime 'n
, not 'v
:
impl<'v, T> Iterator<&'v mut T> for VecMutIterator<'v, T> {
// Type from trait: ^~~~~~~~~
fn next<'n>(&'n mut self) -> Option<&'n mut T> {
// Required return type: ^~~~~~~~~
...
}
}
Of course, this is not possible, because the return type is specified
by the Iterator
trait to be Option<A>
where A
is the type
parameter supplied to the Iterator
trait (in this case,
A=&'v mut T
). Moreover, we can’t just change the impl to use the lifetime 'n
,
because 'n
is only in scope on the next()
method. In fact,
for any given VecMutIterator
instance, there isn’t a single lifetime
'n
but rather a distinct lifetime 'n
for each call to next()
,
so there is no way we could put 'n
into the VecMutIterator
type itself.
So what can we do about it?
OK, we’ve now seen that in fact the VecMutIterator
implementation I
showed you is unsafe and couldn’t be implemented. But we do want to
have an iterator over mutable references. So what are our
alternatives? In the beginning of the article, I outlined three
possibilities, and I want to describe them now in more detail.
Option 1. Use privacy and an unsafe implementation.
Interestingly, the specific impl I showed you is only unsafe if users
violate the intended interface. That is, if people only call the
next()
method, they will always be supplied with a fresh mutable
reference each time. Problems arise only when users reach into the
iterator itself and create new aliases into the slice that it
encapsulates. But we have the means to prevent that: privacy.
We could decide the iterator type like so:
// 'v: lifetime of the vector
struct VecMutIterator<'v, T> {
priv data: &'v mut [T],
priv index: uint,
}
Using this definition, users of VecMutIterator
cannot directly
access data
, and instead are limited to using the next()
method.
Of course, the borrow checker doesn’t understand or consider
privacy, so the implementation of next()
would still yield
type errors. The solution there would be to use unsafe code:
impl<'v, T> Iterator<&'v mut T> for VecMutIterator<'v, T> {
fn next<'n>(&'n mut self) -> Option<&'v mut T> {
let index = self.index;
self.index += 1;
if index < self.length {
// Note: the lifetime `'n` is shorter than what we
// want, but it's the only thing that the borrow
// checker can prove.
let ptr: &'n mut T = &mut self.data[index];
// But we know that we never hand out same ref twice,
// and there is no alternate means of accessing `self.data`,
// so we can cheat and extend the lifetime by fiat:
unsafe { Some(unsafe::copy_mut_lifetime(self.data, ptr)) }
} else {
None
}
}
}
This solution is pragmatic but unsavory. It’s safe and convenient for
the user of the interface. It does mean that implementing iterators
over mutable references would always require an unsafe
keyword (and
hence more complex reasoning than normal), unless you are able to
build upon another iterator. An example where the latter is sufficient
would be the Hashmap
type, which stores its data in a vector and
hence can utilize VecMutIterator
to do the actual traversal.
There is some amount of precedent here. In general, the borrow checker
is not smart enough to reason about indices, which means that operations
like mut_split
have traditionally been defined with a safe
interface but unsafe implementation:
impl<T> [T] {
fn mut_split<'n>(&'n mut self) -> (&'n mut [T], &'n mut [T]) {
// Divides a single `&mut [T]` slice into two disjoint slices,
// one covering the left half of the slice and one the right.
...
}
}
The difference to me between these cases is that the reasoning about
whether MutVecIterator
is correct is more complex, since it requires
thinking not about what might happen in the course of a single
function call, but rather over all possible uses of the
MutVecIterator
struct for its entire lifetime (including unintended
uses, as we have seen).
Another consideration is more complex iterators. For example, any
iterator trait that did not preserve the invariant that every item is
returned exactly once (e.g., Java’s ListIterator
, or the
RandomAccessIterator
) is just not compatible with
mutable references unless it is designed very carefully to limit the
lifetime of its return values (rather like the trait I describe in the
next section). However, in Rust we mostly we make use of the
Iterator
and DoubleEndedIterator
traits, which only require any
given element in the iteration space to be returned once (of course
one can have infinite iterators, but one can also choose not
to). This is partly a consequence of Rust’s use of affine types,
meaning they cannot be aliased and must be moved from place to place
(mutable references are of course an example of such a type).
Option 2. Use a different trait for mutable references.
Another option is to create a new trait MutIterator
for iterating
over mutable references. In this case, the existing trait (Iterator
)
would be used for by-value and by-immutable-ref iteration. What these
two cases have in common is that the lifetime of the thing being
iterated over is independent from the iterator itself. In contrast,
the MutIterator
trait would be designed to express that the lifetime
of the things you iterate over is linked to the iterator:
trait MutIterator<A> {
fn next<'n>(&'n mut self) -> Option<&'n mut A>;
}
Here you can see that the lifetime of the result is always 'n
.
This solution permits safe implementations but is less convenient for
end users. You can’t write a single function or type that operates
over any iterator, but must instead always handle the MutIterator
case separately. So things like vec.iter().enumerate()
would likely
become vec.mut_iter().mut_enumerate()
or some such. For better or
worse, though, separating out mutability into its own world is quite
common in Rust libraries, precisely because of the dangers to safety
inherent in mutation, so to some extent this is a natural solution.
Also, at least in our compiler and standard libraries, uses of
mut_iter()
are rather simple and isolated, so having a distinct
trait with fewer capabilities would likely pose little problem.
Having a MutIterator
trait that is distinct from Iterator
would
complicate the for
loop, since it would not be able to assume that
the thing being iterated over must implement Iterator
. We could
either (1) have a for mut
syntax; (2) keep the current “duck-typing”
implementation; or maybe (3) try Iterator
and then
MutIterator
. But “try-this-and-then-that” style reasoning interacts
poorly with type inference so I’d prefer to avoid it (though we
certainly do it at times).
Option 3. Extend the type system.
There are various ways we could extend the type system to resolve this dilemna.
Higher-kinded types. One solution to address the problem of how
the type parameter to the Iterator
trait can refer to the lifetime
'n
that appears below is to add some sort of
higher-kinded types. We could redefine the Iterator
trait so
that it accepts a higher-kinded type parameter. Of course I have no
idea what the syntax would look like, but it might be something like:
trait Iterator<A<'n>> {
fn next<'n>(&'n mut self) -> Option<A<'n>>;
}
Unlike Haskell, which primarily offers kinds of *
for a simple type
and * => K
for a higher-kinded type, we would add a kind like LT
for lifetime. So the kind of A
would be LT => *
(given a lifetime,
you get a simple type).
We could then define the MutVecIterator
as something like
impl<'v, T> Iterator<|'n| &'n mut T> for VecMutIterator<'v, T> {
fn next<'n>(&'n mut self) -> Option<&'n mut T> {
....
}
}
Now the type being iterated over is |'n| &'n mut T
– I am using
||
to copy our closure syntax, since this is effectively a function
where the parameters and result types are types, not values.
Anyway, there is clearly a fair amount of design work to be done, and
not to mention consideration of the ergonomics. The above notations
are somewhat intimidating. I also do not think this can be done in a
backwards compatible way – that is, the existing VecMutIterator
which today requires an unsafe implementation would not be typable.
This is because the existing version returns &'v mut T
values, but
the HKT version would be returning &'n mut T
values.
Theorem proving. In principle, we could eventually integrate some
kind of optional theorem proving into rustc to enable it to reason
about array indices and time more extensively. Such an extension would
definitely allow something like split
to be proven safe, and would
probably allow VecMutIterator
as well. But the VecMutIterator
proof would require a more extensive integration, since it would
require reasoning about privacy etc.
Some considerations
I think the choice is down to (1) unsafe implementations or (2) a
distinct MutIterator
trait. I honestly don’t know where I fall
yet. Here are the primary considerations that I have been pondering.
Safety. To be honest, I am not that concerned about the safety impliciations of requiring unsafe implementations for mutable iterators. Many data structures can just build on vectors and hashtables, and so their iterators would be safe. For the rest, well, data structures in general seem to be a prime place where unsafe code makes sense – they offer constrained, well-specified interfaces; they are widely used and efficiency is paramount; and there is a long history of efficient, novel data structures that the type system could never hope to capture.
Convenience and flexibility. Choosing an unsafe impl but safe
interface yields the most convenience for end-users. Not only can you
write generic code that operates over all iterators, but the mutable
references you iterate over have longer lifetimes than they would in
the MutIterator
trait approach. For example, the following snippet
of code works for the unsafe implementation but not the MutIterator
trait:
let mut iter = VecMutIterator { ... };
let ptr0 = iter.next().get();
let ptr1 = iter.next().get();
// ptr0 and ptr1 co-exist now
With a MutIterator
, you would not be able to call next()
until
ptr0
had gone out of scope. With the unsafe impl approach, ptr0
remains in scope as long as the slice that the iterator encapsulated.
But traits like RandomAccessIterator
cannot be supported,
since they would only be safe with a shorter lifetime.
Future proofing. On the other hand, precisely because it is offers
a more limited API, I think that the MutIterator
trait is more
“future-proof”. Choosing MutIterator
would ensure that there are no
Iterator
implementations for mutable references now. If we later
extended the type system in some way so as to make such
implementations checkable, we could then add iterators for &mut T
references in whatever form these extensions permit, and deprecate
MutIterator
. In particular, if we added HKT, which seems more likely
than theorem proving, we could add iterators that only permit
iteration over &'n mut T
. Such iterators could also support
the RandomAccessIterator
trait.
Conclusion
I kind of hate it when blog posts or news articles address the reader in the last paragraph, since it generally seems like a rather formulaic and pedestrian way of creating user interaction. But, in this case, it seems like the right way to end the post, so I’ll make an exception: Tell me dear reader, what do you think we should do?