You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Similarly to the previous post, we will once again add types to the Rust code which works perfectly fine without them. This time, we’ll try to improve the pervasive pattern of using indexes to manage cyclic data structures.
The problem
Often one wants to work with a data structure which contains a cycle of some form: object foo references bar, which references baz which references foo again. The textbook example here is a graph of vertices and edges. In practice, however, true graphs are a rare encounter. Instead, you are more likely to see a tree with parent pointers, which contains a lot of trivial cycles. And sometimes cyclic graphs are implicit: an Employee can be the head of a Departement, and Departement has a Vec<Employee> personal. This is sort-of a graph in disguise: in usual graphs, all vertices are of the same type, and here Employee and Departement are different types.
Working with such data structures is hard in any language. To arrive at a situation when A points to B which points back to A, some form of mutability is required. Indeed, either A or B must be created first, and so it can not point to the other immediately after construction. You can paper over this mutability with let rec, as in OCaml, or with laziness, as in Haskell, but it is still there.
Rust tends to surface subtle problems in the form of compile-time errors, so implementing such graphs in Rust is challenging. The three usual approaches are:
arena and real cyclic references, explanation by simonsapin (this one is really neat!),
arena and integer indices, explanation by nikomatsakis.
(apparently, rewriting a Haskell monad tutorial in Rust results in a graphs blog post).
I personally like the indexing approach the most. However it presents an interesting readability challenge. With references, you have a foo of type &Foo, and it is immediately clear what that foo is, and what you can do with it. With indexes, however, you have a foo: usize, and it is not obvious that you somehow can get a Foo. Even worse, if indexes are used for two types of objects, like Foo and Bar, you may end up with thing: usize. While writing the code with usize actually works pretty well (I don’t think I’ve ever used the wrong index type), reading it later is more complicated, because usize is much less suggestive of what you could do.
Newtype trick
One way to ameliorate this problem is to introduce a newtype wrapper around usize:
Here, “one should use FooIdx to index into Vec<Foo>” is still just a convention. A cool thing about Rust is that we can turn this convention into a property verified during type checking. By adding an appropriate impl, we should be able to index into Vec<Foo> with FooIdx directly:
It’s insightful to study why this impl is allowed. In Rust, types, traits and impls are separate. This creates a room for a problem: what if there are two impl blocks for a given (trait, type) pair? The obvious choice is to forbid to have two impls in the first place, and this is what Rust does.
Actually enforcing this restriction is tricky! The simplest rule of “error if a set of crates currently compiled contains duplicate impls” has severe drawbacks. First of all, this is a global check, which requires the knowledge of all compiled crates. This postpones the check until the later stages of compilation. It also plays awfully with dependencies, because two completely unrelated crates might fail the compilation if present simultaneously. What’s more, it doesn’t actually solve the problem, because the compiler does not necessary know the set of all crates beforehand. For example, you may load additional code at runtime via dynamic libraries, and silent bad things might happen if you program and dynamic library have duplicate impls.
To be able to combine crates freely, we want a much stronger property: not only the set of crates currently compiled, but all existing and even future crates must not violate the one impl restriction. How on earth is it possible to check this? Should cargo publish look for conflicting impls across all of the crates.io?
Luckily, and this is stunningly beautiful, it is possible to loosen this world-global property to a local one. In the simplest form, we can place a restriction that impl Foo for Bar can appear either in the crate that defines Foo, or in the one that defines Bar. Crucially, whichever one defines the impl has to use the other, which makes it possible to detect the conflict.
This is all really nifty, but we’ve just defined an Index impl for Vec, and both Index and Vec are from the standard library! How is it possible? The trick is that Index has a type parameter: trait Index<Idx: ?Sized>. It is a template for a trait of sorts, and we get a “real” trait when we substitute type parameter with a type. Because FooIdx is a local type, the resulting Index<FromIdx> trait is also considered local. The precise rules here are quite tricky, this RFC explains them pretty well.
More impls
Because Index<FooIdx> and Index<BarIdx> are different traits, one type can implement both of them. This is convenient for containers which hold distinct types:
structArena{foos:Vec<Foo>,bars:Vec<Bar>,}
implops::Index<FooIdx>forArena{...}
implops::Index<BarIdx>forArena{...}
It’s also helpful to define arithmetic operations and conversions for the newtyped indexes. I’ve put together a typed_index_derive crate to automate this boilerplate via a proc macro, the end result looks like this:
#[macro_use]externcratetyped_index_derive;
structSpam(String);
#[derive( // Usual derives for plain old data Debug,Copy,Clone,Ord,PartialOrd,Eq,PartialEq,Hash,
<span>TypedIndex</span>
)] #[typed_index(Spam)]// index into &[Spam] structSpamIdx(usize);// could be u32 instead of usize
<span>// Conversions between `usize` and `SpamIdx`</span>
<span>let</span> <span>idx</span><span>:</span> <span>SpamIdx</span> <span>=</span> <span>1</span><span>.into</span><span>();</span>
<span>assert_eq!</span><span>(</span><span>usize</span><span>::</span><span>from</span><span>(</span><span>idx</span><span>),</span> <span>1</span><span>);</span>
<span>// Indexing `Vec<Spam>` with `SpamIdx`, `IndexMut` works as well</span>
<span>assert_eq!</span><span>(</span><span>&</span><span>spams</span><span>[</span><span>idx</span><span>]</span>.<span>0</span><span>,</span> <span>"bar"</span><span>);</span>
<span>// Indexing `Vec<usize>` is rightfully forbidden</span>
<span>// vec![1, 2, 3][idx]</span>
<span>// error: slice indices are of type `usize` or ranges of `usize`</span>
<span>// It is possible to add/subtract `usize` from an index</span>
<span>assert_eq!</span><span>(</span><span>&</span><span>spams</span><span>[</span><span>idx</span> - <span>1</span><span>]</span>.<span>0</span><span>,</span> <span>"foo"</span><span>);</span>
<span>// The difference between two indices is `usize`</span>
<span>assert_eq!</span><span>(</span><span>idx</span> - <span>idx</span><span>,</span> <span>0u</span><span>size</span><span>);</span>
Newtype Index Pattern
https://ift.tt/2LkYmFd
Similarly to the previous post, we will once again add types to the Rust code which works perfectly fine without them. This time, we’ll try to improve the pervasive pattern of using indexes to manage cyclic data structures.
The problem
Often one wants to work with a data structure which contains a cycle of some form: object
foo
referencesbar
, which referencesbaz
which referencesfoo
again. The textbook example here is a graph of vertices and edges. In practice, however, true graphs are a rare encounter. Instead, you are more likely to see a tree with parent pointers, which contains a lot of trivial cycles. And sometimes cyclic graphs are implicit: anEmployee
can be the head of aDepartement
, andDepartement
has aVec<Employee>
personal. This is sort-of a graph in disguise: in usual graphs, all vertices are of the same type, and hereEmployee
andDepartement
are different types.Working with such data structures is hard in any language. To arrive at a situation when
A
points toB
which points back toA
, some form of mutability is required. Indeed, eitherA
orB
must be created first, and so it can not point to the other immediately after construction. You can paper over this mutability withlet rec
, as in OCaml, or with laziness, as in Haskell, but it is still there.Rust tends to surface subtle problems in the form of compile-time errors, so implementing such graphs in Rust is challenging. The three usual approaches are:
(apparently, rewriting a Haskell monad tutorial in Rust results in a graphs blog post).
I personally like the indexing approach the most. However it presents an interesting readability challenge. With references, you have a
foo
of type&Foo
, and it is immediately clear what thatfoo
is, and what you can do with it. With indexes, however, you have afoo: usize
, and it is not obvious that you somehow can get aFoo
. Even worse, if indexes are used for two types of objects, likeFoo
andBar
, you may end up withthing: usize
. While writing the code withusize
actually works pretty well (I don’t think I’ve ever used the wrong index type), reading it later is more complicated, becauseusize
is much less suggestive of what you could do.Newtype trick
One way to ameliorate this problem is to introduce a newtype wrapper around
usize
:Here, “one should use
FooIdx
to index intoVec<Foo>
” is still just a convention. A cool thing about Rust is that we can turn this convention into a property verified during type checking. By adding an appropriate impl, we should be able to index intoVec<Foo>
withFooIdx
directly:The impl would look like this:
Coherence
It’s insightful to study why this impl is allowed. In Rust, types, traits and impls are separate. This creates a room for a problem: what if there are two impl blocks for a given (trait, type) pair? The obvious choice is to forbid to have two impls in the first place, and this is what Rust does.
Actually enforcing this restriction is tricky! The simplest rule of “error if a set of crates currently compiled contains duplicate impls” has severe drawbacks. First of all, this is a global check, which requires the knowledge of all compiled crates. This postpones the check until the later stages of compilation. It also plays awfully with dependencies, because two completely unrelated crates might fail the compilation if present simultaneously. What’s more, it doesn’t actually solve the problem, because the compiler does not necessary know the set of all crates beforehand. For example, you may load additional code at runtime via dynamic libraries, and silent bad things might happen if you program and dynamic library have duplicate impls.
To be able to combine crates freely, we want a much stronger property: not only the set of crates currently compiled, but all existing and even future crates must not violate the one impl restriction. How on earth is it possible to check this? Should
cargo publish
look for conflicting impls across all of the crates.io?Luckily, and this is stunningly beautiful, it is possible to loosen this world-global property to a local one. In the simplest form, we can place a restriction that
impl Foo for Bar
can appear either in the crate that definesFoo
, or in the one that definesBar
. Crucially, whichever one defines the impl has to use the other, which makes it possible to detect the conflict.This is all really nifty, but we’ve just defined an
Index
impl forVec
, and bothIndex
andVec
are from the standard library! How is it possible? The trick is thatIndex
has a type parameter:trait Index<Idx: ?Sized>
. It is a template for a trait of sorts, and we get a “real” trait when we substitute type parameter with a type. BecauseFooIdx
is a local type, the resultingIndex<FromIdx>
trait is also considered local. The precise rules here are quite tricky, this RFC explains them pretty well.More impls
Because
Index<FooIdx>
andIndex<BarIdx>
are different traits, one type can implement both of them. This is convenient for containers which hold distinct types:It’s also helpful to define arithmetic operations and conversions for the newtyped indexes. I’ve put together a
typed_index_derive
crate to automate this boilerplate via a proc macro, the end result looks like this:Discussion on /r/rust.
via matklad.github.io https://ift.tt/2yMSpNn
November 2, 2018 at 11:36AM
The text was updated successfully, but these errors were encountered: