Skip to content

Instantly share code, notes, and snippets.

@cessen
Last active January 26, 2018 22:33
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save cessen/394829673855e56157f63b4447f91e67 to your computer and use it in GitHub Desktop.
Save cessen/394829673855e56157f63b4447f91e67 to your computer and use it in GitHub Desktop.
#Rust2018 blog post

#Rust2018

This is my contribution to the call for Rust blog posts.

I've been using Rust for various personal projects for quite a while now, and I confess that I have a lot to say about it! In fact, I've already said quite a bit about it before.

My experience with Rust has been overwhelmingly positive, and most of the things I still want (e.g. NLL, const functions, const generic parameters, SIMD intrinsics, etc.) are already well known and being worked on, so it seems silly to repeat them. Therefore, in this post I'm just going to highlight three small-ish things that have been mildly frustrating to me over the last year, but which I haven't seen mentioned recently.

BidirectionalIterator trait

I've been working on an editable text buffer library called Ropey, and it's turning out quite well, I think! It's intended to be the backing text buffer for applications such as text editors.

One of the things it provides is various kinds of iterators over the contents of the buffer, e.g. over its bytes and chars. One of the intended uses for these iterators is to allow for efficient textual searches, syntax highlighting processing, etc. However, some search algorithms (e.g. backtracking regex) need to be able to move backwards through an iterator, not just forwards. So I would like to implement Ropey's iterators to be bidirectional.

Before I go any further, I'd like to pause for a moment to first acknowledge that Rust's stdlib iterator support is nothing short of amazing. It's extremely well designed, and because of that it's an absolute delight to work with iterators in Rust.

Nevertheless, stdlib has no BidirectionalIterator trait or equivalent. It has DoubleEndedIterator, but that's very different. This seems to me like just an oversight, because it appears to be pretty straight-forward to provide:

pub trait BidirectionalIterator: Iterator {
    fn prev(&mut self) -> Option<Self::Item>;
}

Maybe there's a reason for its absence. But having a single trait that crates can build on top of for backtracking algorithms would help keep the ecosystem unified going into the future. And that would be a good idea, I think!

Custom slice types

One of my goals with Ropey is for it to have familiar-feeling, ergonomic APIs. Whether I'm achieving that or not is a bit up in the air. But one place where Ropey definitely is not familiar or ergonomic is the way it handles slicing, which currently looks like this:

// `text` here is a buffer from Ropey
do_something_with_a_slice(text.slice(2, 8));

Granted, this isn't awful. But it doesn't look like slicing normally does. Ideally, I would like slicing with Ropey to look like this:

do_something_with_a_slice(&text[2..8]);

That's the visual pattern that Rust programmers expect, and using that familiarity to make Ropey code more immediately visually parsable would be great!

Unfortunately, that isn't currently possible. Ropey doesn't store text in a single contiguous chunk, so it can't return an actual "ptr + len" style slice across arbitrary lengths of the text. Instead, it defines a RopeSlice type that is the equivalent of that, but for how Ropey stores its data. However, you cannot return a RopeSlice from the index operator because the Index trait is defined like this (simplified for readability):

pub trait Index<Idx> {
    type Output;
    fn index(&self, index: Idx) -> &Self::Output;
}

I am not the first person to be frustrated by this (see here and here).

Now... there is actually good reason for the Index trait to be defined this way, and I agree with keeping the semantics of the current index operator. Nevertheless, there are many cases where types that aren't literal references or slices--but which share their important semantics--would need to be returned from the Index operator. RopeSlice is one of those, and currently it can't be.

It would be really nice to see this addressed, though I'm not at all sure what the best approach would be. Maybe adding IndexByValue and IndexByValueMut traits. Or adding a Slice trait for custom slice types, which can then be used in these circumstances... somehow. I'm really not sure. But this situation is definitely a bit bothersome!

Having said all of that, text.slice(2, 8) is definitely not the worst thing in the world.

Stabilize RangeArgument, please!

Being able to return custom slice types from Index is a bit of a hairy problem, so I don't really expect that to be addressed soon. But it would at least be nice to use this syntax for slicing:

text.slice(2..8);

Technically, you can write functions that work that way by having them take the std::ops::Range type as their argument. But then you can't do this:

text.slice(2..);

And... technically you can write functions that work that way by taking the std::ops::RangeFrom type. But you can't do both. More broadly, you have to pick which single one of the following you want to support:

  • a..b
  • a..
  • ..b
  • ..

The solution to this is the std::collections::range::RangeArgument trait, which generalizes over the four range types. Unfortunately, it's still unstable. Which is frustrating.

Having said that, it looks like it was really close to being stabilized, and pretty much just needs someone to push it over the finish line. So maybe I should get my hands dirty and try to finish it up. In fact, come to think of it, that attitude applies to the two other items above as well! So much to do, so little time...

Summary

In summary... Rust is awesome! As I hope is obvious, all three of the above items are pretty much just paper cuts. Rust certainly isn't perfect, but I enjoy coding in it. And it's constantly getting better.

A great, big, huge, enormous thanks to everyone who has contributed to Rust! I owe you all a beer. But probably cheap beer, because there's a lot of you.

@jjpe
Copy link

jjpe commented Jan 10, 2018

Have you looked at Xi? It looks like its core is a lot like your ropey project. Perhaps there is room for collaboration between the 2 projects?

@HadrienG2
Copy link

As for BidirectionalIterator, IIRC the main obstacle is that if Iterator::Item contains an &mut, and you produce a given output twice via a prev/next cycle, you will violate Rust's core "no mutable aliasing" guarantee.

@BatmanAoD
Copy link

Is the BidirectionalIterator issue something you've brought up on the rust lang forum? They seem like reasonable candidates for the RFC process (said the person with no experience submitting or commenting on an RFC).

@cessen
Copy link
Author

cessen commented Jan 26, 2018

@jjpe:
I have! And the latest iterations of Ropey take some inspiration from it. Ropey pre-dates the public announcement of Xi, so I've continued on with it, but I would certainly be open to collaboration. However, I think xi-rope is making some different trade-offs than Ropey, so it may be best for them to stay separate anyway to give people options.

@HadrienG2:
Yeah, implementors would have to be aware of that. Nevertheless, the borrow checker will prevent implementations that violate that (outside of using unsafe code, anyway), so I don't think that's dangerous.

Someone in the reddit thread suggested defining an entirely new trait, Cursor, or something like that. I would also be quite happy with that. I'm not concerned about the specifics, rather about being able to accomplish the use-cases that a bidirectional iterator would be otherwise be used for.

@BatmanAoD:
Not yet, but I may tackle that at some point. Right now I have a bunch of other things on my plate!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment