[Pre-RFC] Add a `Coerce` trait to get rid of the `.as_slice()` calls


#1

Summary

Add a Coerce trait that will let create/update functions to also accept &Vec<T>/&String wherever &[T]/&str is expected.

Motivation

Ergonomics. We get rid of some (most?) of the as_slice() calls. Users would be able to change code that looks like this string.push_str(another_string.as_slice()) to string.push_str(&another_string).

Detailed design

We add a Coerce trait that we’ll let us “coerce” a &Vec<T> into a &[T]. The signature looks like this:

trait Coerce<P> {
    fn coerce(self) -> P;
}

Then we can implement it on &Vec<T> and &[T]:

impl<'a, T> Coerce<&'a [T]> for &'a [T] {
    fn coerce(self) -> &'a [T] {
        self
    }
}

impl<'a, T> Coerce<&'a [T]> for &'a Vec<T> {
    fn coerce(self) -> &'a [T] {
        self.as_slice()
    }
}

Now we can change any function/method that expects a &[T] to actually accept &Vec<T> or &[T]. As an example, the append method would become:

fn append<S: Coerce<&'a [T]>>(mut self, second: S) -> Vec<T> {
   self.push_all(second.coerce());
   self
}

This new append method would allow this:

let v1 = vec!(1u8, 2, 3);
let v2 = vec!(4u8, 5, 6);
let v3 = v1.append(&v2);  // you can still use `v1.append(v2.as_slice())`

(Toy example in the playpen)

This treatment can also be applied to the String/&str pair.

Implementation “plan”

  • &Vec<T> and &[T] will implement Coerce<&[T]>
  • &String and &str will implement Coerce<&str>
  • Update all the functions/methods in the standard library from accepting &str/&[T] to accept S where S: Coerce<&str>/S: Coerce<&[T]>.

This last step should break a minimal amount of code (I’m not 100% sure), because the updated functions would still accept &[T]/&str.

Drawbacks

  • The signature of functions/methods become more complex.

Alternatives

Don’t do this, and use of the following alternatives:

Advantages of this proposal over the alternatives

  • Can be implemented right now.
  • It doesn’t need a lang item nor DST.
  • Shortest notation: &string vs &*string vs string[..] vs string.as_slice()

Ultimately if this doesn’t get implemented in the standard library, users can still implement this idea in their libraries.

Open questions

  • &mut Vec<T> could implement Coerce<&'a mut [T]>, but I’m not sure if that’s actually useful.

This just occurred to me in the morning, I figured I’d post it as a Pre-RFC to get feedback on the idea. I’m especially interested in hearing why do you think we should not do this.


#2

There is currently Str and Vector traits that functions can be generic over.


#3

Yes, you could make generic functions using Slice, but you’ll have to pass references to slices:

fn append<S: Slice<T>>(mut self, second: &S) -> Vec<T> {
   self.push_all(second.as_slice());
   self
}

v.append(&vector);
v.append(&slice);
v.append(&&[1, 2, 3])

I’ll prefer to use v.append(&[1, 2, 3])


#4

Thanks for writing this up! I’ve been thinking about some similar things as well.

I think the signature complexity here is a serious drawback, given how common it is to take slice arguments. You also have a somewhat irritating prelude in any such API where you call .coerce() on all of the “slice-ish” arguments.

But it’s also worth noting a related drawback: by making functions generic in this way, you get codesize blowup if you call with both slices and vectors, even though the difference between the two is just the semantics of the prelude mentioned above.

It also means that you only get ergonomic improvements for APIs that opt in to this setup.

That said, the more general idea of having standard traits encompassing conversions is probably a good one, and with the multidispatch proposal would be possible. (Without multidispatch, you can only give one target type for conversion, which is ok for slicing but not other kinds of conversions.)

In the long run, we may want to consider implicit coercions based on this kind of trait mechanism, but that would mean running arbitrary code at many more points than we currently do – it’s a big step that we’d want to consider carefully.


#5

I’ve been playing around with this idea here. I was at one point using it in Iron but have instead gone for something more specialized. This would be much better with multidispatch.


#6

Generic coercion is good, but it needs to happen at the call site, not in the function implementation.


#7

I have an idea that if the multiple dispatch feature landed, we could add an As trait to rust:

trait As<T> {
    fn as(&self) -> T
}

which will be invoked when we write, for example, 1i as uint.

And we can impl As<&str> as well as As<String> for int. So that the code like 1i as &str and 1i as String would be possible and convenient to convert int to borrowed string and owned string.


#8

I agree.

I didn’t think about this when I wrote the RFC, but this is a serious problem. I think that something like this may help to avoid that:

#[inline(always)]
fn append<S: Coerce<&'a [T]>>(mut self, second: S) -> Vec<T> {
    common(self, second.coerce())

    fn common(mut v: Vec<T>, second: &[T]) -> Vec<T> {
        v.push_all(second);
        v
    }
}

That sort of replicates an as_slice call at each append method call, but it adds too much boilerplate, and may not always work as expected.

I’m not 100% sure about having implicit coercions. I think it may be too much “magic”, but mainly because it affects documentation: string.contains("needle") would work because string gets coerced to &str, but String doesn’t implement the contains method, and doesn’t appear in the documentation. That can be surprising. Also coercions are cheap but they are not free, so they may affect performance if used (several times) (and without notice) in hot code paths.


At the end, I’m starting to think that the [..] sugar may be the best solution to the as_slice ergonomics problem.