Function overloading?, e.g. a.extend(b) vs a.extend(&b)

How is a.extend(b) and a.extend(&b) having different implementations not function overloading? I thought Rust didn't have function overloading. The Extend trait is "specialized" for both value and reference arguments for the same Vec<T,A> type. What kinds of argument type overloading are possible using traits?

Rust doesn’t have the style of overloading that involves just defining two things with the same name and letting the compiler figure it out. Traits do support quite flexible overloading though. So the question becomes what you do or don’t call “overloading”.

With traits, you can overload based on the types of self as well as any argument, and the only limitation a trait puts on this overloading comes from the way the trait defines the methods itself (well… and also, even for a trait offering a maximally generic signature in a method, if you aren’t in the same crate where the trait was defined, there’s the orphan rules to take into consideration).

Something that traits won’t do is offer overloading based on arity (number of arguments). So .extend here always has a single argument.

A typical example for a highly flexible overloadable kind of method is the operator traits for things like + (the std::ops::Add trait), - (std::ops::Sub), * (Mul), / (Div), % (Rem), << (Shl), >> (Shr). Their respective single method allows you to freely choose the right-hand-side argument, and return type; though the standard library & Rust language authors decided to leave one limitation in place, that the argument types must determine the result type.[1]


Looking at x.extend(y) in particular, the implementation of the Extend trait isn’t the only thing that allows multiple right-hand-side argument types directly. You need to look at the signature

pub trait Extend<A> {
    fn extend<T: IntoIterator<Item = A>>(&mut self, iter: T);
}

and you can see that the iter argument is always allowed to be generic, i.e. it’s sort-of overloaded for different kinds of iterator or collection types.

This allows you to do

fn main() {
    let mut a = vec![1, 2, 3];
    let b = std::collections::HashSet::from([5, 55, 555]);
    let c = [10, 20, 30];
    let d = vec![42; 10];

    a.extend(b); // with HashSet<i32>
    a.extend(c); // with [i32; 3]
    a.extend(d); // with Vec<i32>
}

based on a single implementation of Extend<i32> for Vec<i32> (the full implementation is generic and implements Extend<T> for Vec<T> for all types T) relying on IntoIterator implementations for HashSet and for arrays and for Vec.

However, there’s a second Extend implementation for Vec<i32>, namely an Extend<&i32> one.

This is what your mentioned example of a.extend(b) vs. a.extend(&b) for a: Vec<T> seems to be about.

This second implementation of Extend for Vec<i32> is, really, just a convenience implementation of the general form Extend<&T> for Vec<T> where T: Copy. The need to copy elements is because the iterator only provides &T items (references to T) here, and the Vec<T> wants owned T values. The limitation to Copy instead of T: Clone, which would have been more general, is because it’s a rather implicit thing and we wanted to avoid any accidental expensive cloning here.

Let’s take some code now…

fn main() {
    let mut a = vec![1, 2, 3];
    let b = vec![10, 11, 12];

    a.extend(&b);
    a.extend(b);
}

The second step is to look at the relevant IntoIterator implementations again that connect the types i32 and &i32 to the types of b and &b, respectively. The a.extend(&b) call is powered by the abovementioned Extend<&i32> for Vec<i32> implementation (here Vec<i32> matches the type of a), combined with a IntoIterator<&i32> for &Vec<i32> one (here &Vec<i32> matches the type of &b), of course this one is generic, too, and works generally to allow IntoIterator<&T> for &Vec<T>.

I call it a convenience implementation, because we can easily re-write the a.extend(&b) call so it doesn’t need the second Extend<&i32> for Vec<i32> implementation anymore, by turning &b into an iterator of owned i32 values, as follows.

a.extend(b.iter().copied());

If you have a T: Clone type that doesn’t support Copy, this approach can be generalized to use the .cloned() method of Iterator, so that

fn main() {
    // `Box<i32>` does not implement `Copy
    let mut a = vec![Box::new(1), Box::new(2), Box::new(3)];
    let b = vec![Box::new(10), Box::new(11), Box::new(12)];

    // a.extend(&b); // doesn't work
    // a.extend(b.iter().copied()); // doesn't work
    a.extend(b.iter().cloned()); // works
    a.extend(b);
}

  1. This is not a limitation to overloading in Rust, but one that comes from how the Output types are chosen to be associated types. I.e. it’s also possible to have an output type be a trait’s type parameter, and then to overloading over the return type, something that a lot of other programming languages don’t even support, so Rust might be even more flexible here than some other languages. ↩︎

12 Likes

Wow! Thanks for your very comprehensive answer. This sort of raised a curtain for me to reveal a new horizon.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.