More consistency in std::vec::Vec

I generally find that the functions on std::vec::Vec are somewhat inconsistent. Before considering to propose any changes to this, I want to find out if people agree that it’s not quite perfect… Also, had there been a discussion of this already and whether DST may affect this in any significant way?

Is Vec inconsistent to what? Vec is one-of-a-kind type in Rust after all.

Being less vague by providing examples of the inconsistencies would help people provide feedback.

1 Like

Apologies for being vague.

From the top down:

  • grow - takes a slice… why it cannot take a Vec?
  • tail and tailn - return a slice… why there is no version that would return a Vec? and how about head and headn?
  • last - why there is no first?
  • push_all - again, only takes a slice
  • init - don’t even understand why this function is needed and why it’s call so… and again it does return a slice, not a Vec

Other counterintuitive naming choices are push, unshitf and append

From this it doesn’t seem like Vec is an immutable sort of structure, I’d much rather like to see something close to Clojure, or otherwise something more minimal, like with Python list you only have: append, count, extend, index, insert, pop, remove, reverse, sort - and that’s rather enough. I understand that Rust is not dynamic, growing would be an addition to the above, but otherwise I’m pretty confused by the set of methods a Vec provides.

  • grow doesn’t take a slice, it takes a reference.
  • tail and tailn don’t return Vec because they create a view into the last elements of a vector. To convert a slice to a vector, use slice.to_vec().
  • I don’t know why there is no first. (Something makes me think that this may have been removed recently.) That seems like it could be a reasonable addition, but it would be much more useful to have a method that returns Option<&'a T> for any index.
  • push_all takes a slice because slices are much more useful than Vecs for pasing to functions. Any Vec can be converted to a slice, but converting from a slice to a Vec can be an expensive operation, as it has to allocate.
  • init probably comes from languages like Haskell, where that is the standard name for such a method. init, tail etc. can be used quite a lot in functional programming to do with lists. It returns a slice for the same reason tail does (see above).

All these methods are here to remove commonly-seen boilerplate code to do with lists. It’s significantly nicer to use std::os::args().tail() to get the command-line arguments passed to your program than std::os::args().slice_from(1) or even let args = std::os::args(); args.slice(1, args.len()).

Eh, I feel like args().slice_from(1) is almost better, since I can read it and know immediately what it does. tail is more ambiguous.

Most of the methods you list there on Vec are for ergonomic reasons, to avoid having to write the .as_slice in some_vec.as_slice().foo() so much (by putting the common ones directly on Vec). With DST we can likely remove them and instead have a Deref<[T]> for Vec<T> implementation, which will allow the &[T] methods to be called automatically without being explicitly part of the Vec API (although you may not regard this as a good thing).

Returning a Vec from all those functions would be really really inefficient. Returning a slice is just returning a pointer into the memory owned by the Vec, but returning a Vec would require allocating new memory and cloning everything (or consuming the old Vec and shifting pointers/byte-copying data around, which is still inefficient, especially for tail). This slow behaviour copy-ful can be achieved with x.tail().to_vec() (etc.).

first is easily implemented as x[0] (well, really, if/when we move get to return an Option, x.get(0)); last is more complicated/annoying/easier to get wrong: x.get(x.len() - 1).

In what way are push and append unintuitive?

There's .push_all_move which takes a Vec, both are somewhat useful, they can (and possibly should) be removed in favour of the more flexible .extend method:

v.extend(slice.iter().map(|x| x.clone()));
v.extend(vec.move_iter())

We would still need some form of mutable Vec. It's a equivalent to C++'s std::vector (and can be more efficient due to Rust not having copy constructors!), with essentially the same functionality, as a (somewhat) general purpose sequence container that other structures can be built on (e.g. PriorityQueue and (previously) HashMap). An efficient immutable/persistent vector type is a different data structure, it is desirable but not a replacement for Vec.

I agree that the Vec API could use some adjustment/trimming (although I weakly recommend we wait until after DST before embarking in earnest), but having the 'flavourful' methods is nice, as you don't have to write (and debug) them when you do need them. And if necessary, they can be optimised using unsafe code by the top Rust experts (with code-review by other top Rust experts); I think it's a reasonable statement that unsafe code outside rust-lang/rust is less likely to be correct, on average.

(Another approach we could/should investigate is seeing if we can turn some of the Vec algorithms into more generic Iterator ones, similar to C++.)

2 Likes

On Fri, Aug 01, 2014 at 10:27:38PM +0000, errordeveloper wrote:

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.