Discussing some aspects of Method Calls

Chapter 5 of the book talks about Automatic Dereferencing with the notable example of method calls.

Call Expressions have a search-process to find the method in an instance.method() call. This algorithm enables ergonomic read and use since (*instance).method(), (&instance).method() and such are later added by the compiler.

For example in instance.method() the compiler will know the type of instance but the guessing-feature (par above) doesn't know whether it's for instance, *instance, &instance, &mut instance...

So it creates candidates until the it matches, say fn method(&mut self).

A complete example of that process is given in the reference:

For instance, if the receiver has type Box<[i32;2]> , then the candidate types will be Box<[i32;2]> , &Box<[i32;2]> , &mut Box<[i32;2]> , [i32; 2] (by dereferencing), &[i32; 2] , &mut [i32; 2] , [i32] (by unsized coercion), &[i32] , and finally &mut [i32] .

  • My first question is: Are the candidate types independent of the type of the instance ? In other words, do instance, &instace, &&instance all generate the same candidate list? I assume so, or that one is a subset of the others, but I'm unsure.
  • PS: What happens if 2 of the types in the list have the same method? Like x and &x ? (or it could be that the sentence below is correct, I am hesitating now.)

From that candidate list only one matches the receiver's type (that in the method signature.)

Case 1: No traits
Expectedly so, this fails to compile:

Snippet 1: No Traits
struct A { }

impl A {
  fn foo(self) {}
  fn foo(&mut self) {}
  fn foo (&self) {}
}

However, this overloading of foo is actually allowed for traits (and compounded by A's foo as well):

Case 2: Default Traits

Snippet 2: Default Implementations
struct A;

impl A {
    // not so relevant but left for completeness 
    fn foo(self) {}
}
impl B for A {} // implements the defaults for brevity
impl C for A {} // ditto
trait B {
    fn foo(&self) {}
}
// note that this does NOT clash with B
// due to the search process
trait C {
    fn foo(&mut self) {}
    // fn foo(&self) {} // this would clash with B
}

I think this is error prone. If you Derive a trait, or simply implement an external trait with a default {} that could lead to bugs since a.foo() could be choosing methods from any implemented traits.

Some of the rules I found

  • Compiler only complains when they resolve to the same method and the same receiver; a name+method like foo(&self) in B and C clashes, but won't if one is not &self.
    • When they clash, one needs to disambiguate the code with a full path B::foo(&c) or C::..
  • Compiler takes first self then &self then &mut self; this is important for homonymous methods.

There isn't much of a question for this last part, but it is just very confusing, so any notes, remarks or corrections are welcome.

Apologises for initial formatting issues and an extra unnecessary code snippet. That should be more correct now.


To comment on my own first question:

My first question is: Are the candidate types independent of the type of the instance ? In other words, do instance, &instance, &&instance all generate the same candidate list? I assume so, or that one is a subset of the others, but I'm unsure.

isn't hard to answer just following the algorithm. So they generate similar types, but may not be the same, and won't be exactly the same order in general. Or it seems so to me now.

So an initial &T will generate 6 instances whereas a simple T generates 3, or so?

I don't know if this will really help you, but one thing that is interesting about this is that for types that implement Deref (like box for example), the methods on that type aren't written as methods.

It says in the documentation:

Note: this is an associated function, which means that you have to call it as Box::leak(b) instead of b.leak(). This is so that there is no conflict with a method on the inner type.

This is done because the inner type could have a method called leak as well and that would be picked before the method on the inner type. You can see that here:

If you comment out the leak method on Box it will pick the other one.

Thank you!

Methods are associated functions as well:

Specifically, there are associated functions (including methods),

but here they mean "non-method" which are the ones without a receiver (self, &self, &mut self).

Yes it happens to all Box, String(which is Vec), Vec which I assume implement Deref, so one can use *.

So with those the search process is complex. Calling a method (as in b for a.b()) when we have a wrapper Box and a wrapped <T>, then Box is inspected first and T last. So calling Box-like types U we get U, &U, &mut U, and only then *U=T and its variants.

This is both what you understood I think and what the text says, and makes sense.

My confusion is better explained in the examples, but it is related!

Right: 3 extra candidates on the beginning. So let's say the variable you're calling the method on has type &Box<[i32;2]>; the difference in the list of candidates is

+&Box<[i32;2]>, &&Box<[i32;2]>, &mut &Box<[i32;2]>,
 Box<[i32;2]> , &Box<[i32;2]>, &mut Box<[i32;2]>,
 [i32; 2], &[i32; 2], &mut [i32; 2],
 [i32], &[i32] , &mut [i32] 

Side note: the only unsizing operation considered is for arrays to slices. In particular, considering dyn Trait candidates for some &ConcreteType variable, etc, is not part of method resolution.

What generally matters is the type of self on the method. Let's say both of &Box<T> and Box<T> have a method(self), and you use method syntax on a variable that is

  • &Box<T>
    • &Box<T> comes before Box<T> in this candidate list (as shown above), and <&Box<T>>::method takes &Box<T>, so that's the winner
  • &mut Box<T>
    • The list starts out &mut Box<T>, &&mut Box<T>, &mut &mut Box<T>, Box<T>, &Box<T>
    • Box<T> comes before &Box<T> this time, and <Box<T>>::method takes Box<T>, so that's the winner -- different from above! (This will also fail as it will try to move the box out from under the &mut _.)
  • Box<T>
    • Box<T> comes before &Box<T> in this candidate list, so <Box<T>>::method is the winner this time

Now let's say it was clone(&self) -> Self instead. Note how that's the same as clone(self: &Self) -> Self -- the receiver is a reference to the implementing type, not just the implementing type. And we call it on

  • &Box<T>
    • The first candidate is &Box<T> and <Box<T> as Clone>::clone wins -- because self in that case matches the candidate &Box<T>.
  • &mut Box<T>
    • self is never &mut Box<T> so the first candidate doesn't match. &mut _ are not Clone so there is no clone(self: &&mut _) method and the second candidate doesn't match. The third candidate is another &mut _ which doesn't match. The fourth candidate is Box<T>, and self is never Box<T>, so that doesn't match. The fifth candidate is &Box<T> and finally <Box<T> as Clone>::clone wins again.
  • Box<T>
    • self is never Box<T>, so the first candidate doesn't match. The next candidate is &Box<T> and <Box<T> as clone>::clone wins again.
  • &&Box<T>
    • The first candidate matches <&Box<T> as Clone>::clone because shared references &_ do implement Clone (and Copy), so you clone the reference and not the box. Whoops!

Strict typing tends to make scenarios like accidentally calling the wrong clone (&&Box<T>) or method (&mut Box<T>) compiler errors instead of logic bugs, but you may end up doing things like

(&*bx).method();
(*bx).clone();

In all our examples, at most one method matched the candidate at each phase. If more than one method matches the same candidate,[1]

  • the inherent (non-trait) method, if there is one, wins
  • otherwise it's ambiguous and an error is thrown

  1. maybe two traits have a method with the same name and receiver type ↩︎

4 Likes

PS: Thanks for the superb answer.


The first step is to build a list of candidate receiver types. Obtain these by repeatedly dereferencing the receiver expression’s type, (...)

PS: I analysed this in the next reply. I think it's clear now, so I deleted the questions.


In your first a.method(self) example. IIUC for an initial variable of type a:&Box<T> that type wins, and for an initial variable a:Box<T> then Box<T> wins. That's what I feared; if that were a clone (which it's not) then the result is different. Am I reading your example correctly?

So one needs to consider:

  1. Is the method implemented in this type?
    and
  2. Does this type match the "self"/receiver type?

Also, in this part:

The fifth candidate is &Box<T> and finally <Box<T> as Clone>::clone wins again.

Isn't "finally <&Box..." instead of "finally <Box..." ?

I think I now got what the book quote means.

So for a type of Box<T> one first gets Box<T> and T (or so) and then each of these branches into 3, U, &U, &mut U where U is first Box and then T.

Or as a procedure

  1. Gets to Box<T> and by deref to T
  2. Now each branches
    1. Box<T> generates Box<T>, &Box<T>, &mut Box<T>
    2. > generates T, &T, &mut T
  3. Now they are ordered as Box<T>, &Box<T>, &mut Box<T>,T, &T, &mut T

In the &Box<T> case, it starts from there. Like you added in the green line. I interpreted the paragraph as: we first dereference, so that was quite puzzling (in fact I ignored it and simply used the example to understand.)

The results -- as in how the implementing type or the receiver type relate to the the variable type -- are different, yes.

Method Receiver Variable Implementor Receiver Relativity (impl) Relativity (recv)
method self &Box<_> &Box<_> &Box<_> variable variable
clone &self &Box<_> Box<_> &Box<_> *variable variable
method self Box<_> Box<_> Box<_> variable variable
clone &self Box<_> Box<_> &Box<_> variable &variable

Or here's a playground.

The search is looking at the self/receiver type. It considers all applicable implementations.

What defines an applicable implementation is another can of worms I'm sure.

It may all sound awful to you right now, but in practice it's pretty much always fine. You'll get used to it and take it for granted -- take advantage of the auto-ref and auto-deref without thinking about it.

I think the biggest exception are common method names. The most common example of a problem I have seen on this forum is importing the Borrow (or BorrowMut) trait when using Rc<RefCell<_>> or the like. RefCell has borrow and borrow_mut methods, but if you try to call them on an Rc<RefCell<_>>, and Borrow has been imported, the trait will win because literally everything implements Borrow<Self>. So method resolution won't "make it" to the RefCell to find the inherent method. (Even then, the result is typically a compilation error and confusion, versus a runtime bug.)

My solution for this is to never import Borrow at the module level (and rarely at all); instead I spell out the path when I need to.

No, when Box<_> implements clone, &Box<_> is the receiver, because clone takes &self -- aka self: &Self.

Method Receiver Variable Implementor Receiver Relativity (impl) Relativity (recv)
clone &self &mut Box<_> Box<_> &Box<_> *variable &*variable

Or in case this was the confusing part: the syntax is

<ImplementingType as Trait>::method

and the receiver doesn't show up in that syntax.

I can see how that would be confusing. I agree examples are better than just prose for this.

2 Likes

I realised that, besides all that was explained above, part of my confusion was that I thought receiver was the self (in the method parameter) and that the instance as in instance.method() was formally known as the referent.

It is though, defined in the very first line:

A method call consists of an expression (the receiver ) followed by a single dot, an expression path segment, and a parenthesized expression-list.

So I don't know why, but still somehow managed to think it was called referent.

Sometimes though, they seem to be called the same, as in call-expr here:

Several situations often occur which result in ambiguities about the receiver or referent of method or associated function calls.

PS: referent is the value a reference points to, i think.

PS: I misunderstood your reply!

So I took it somehow as the difference between method and non-method.

What you meant, I think, was that box.leak() could otherwise end up calling a method in the inner type of Box<T> due to the search process.

So when it generates Box, &Box, &mut Box, *Box<T>=T, &T, &mut T it may end up calling a method from T. And this indeed will happen in all types that implement Deref I think.

So I assume they do refer to methods (they take self in the docs you linked). However, I am still confused by the:

(...) one thing that is interesting about this is that for types that implement Deref (like box for example), the methods on that type aren't written as methods.

Yet String, Vec also implement Deref so why don't they say anything in their docs ? For example, no such a sentence in String.

Yes, Vec and String implement Deref, but they don't implement deref to T. They implement Deref to [T] or str. Those types are defined in the std and so the rust developers can be sure that [T] and str never have a method called leak.

So the difference is that the deref type is known and it is known that it will never have a conflicting method. Also the leak method on Vec is a normal method so you can write my_vec.leak() instead of Vec::leak(my_vec) like it is for box.

I was a bit unclear about that with "types that implement deref" i meant Box, Arc, Rc and didn't really think about Vec and String, so sorry for confusing you there.

1 Like

But they still implement Deref to something. Are you saying there isn't any method with the same name in String and &str ?

The point of Box::leak and the like is that Box dereferences to some unknown type, which can have any methods it wants. For String/str, both types are known, both are maintained by the same team, so they can guarantee that there are no name clashes.

1 Like

I understand the point, but I don't take guarantees like that unless they are explicitly stated somewhere.

It is more of a Guideline

1 Like

I don't get the process with this particular case. I wonder whether we could discuss just that specific case.

Definitions

The definitions I am using are:

  • Self is the type implementing a method either inherently or from trait.
  • self as a first param marks associated fn as a method. This is not the receiver. The receiver is the caller imho:
    • A method call consists of an expression (the receiver) followed by a single dot, (...)

  • The ref says "The first step is to build a list of candidate receiver types." and potentially coerces the receiver.
    • The receiver type will be coerced to the first candidate-type implementing the method.

Analysing a single case

A type &Box<T> that implemented .clone(&self), guided by the expression &Box<T>::clone(&my_&box_var) so it expects an input: &self = &&Box<T>. There is no look-up beyond the calling type, since it implements the method.

This always fails because &Box<T> does not generate candidates as &&Box<T>.

Further candidates generates are dereferences like Box<T>, &Box<T>. If we use Box<T> which also implements .clone then we need &Box<T> for the self method, but because it has been coerced from the original &Box<T> to Box<T> we do not have it anymore.

So this leads me to a contradiction since that's exactly what you allowed in the example I quoted; I assume that the receiver and self must vary together. But maybe the self and its &-self variants fixed on the type of the initial non-coerced type of the caller ?

Where am I misinterpreting?

I think I may simply ask a new question afresh, so that there aren't more issues dragged on from earlier comments.

I replied over here because that was easier than attempting applying to a multi-party conversation I hadn't joined yet in the other thread. Hopefully my reply isn't too redundant. After this I'll just wait for any further replies you may make (in either thread).

This is the part that is causing confusion. That's also what most the discussion in the other thread is about, I believe.

    let bx: Box<[i32; 2]> = ...;
    bx.method();

The type of bx is what they call the "reciever expression type". To avoid overloading terms, let's call it the (dot) operand type instead.

You build the candidate list based on the operand type. I think you understand the list building at this point. Once you get around to looking at methods, though, you are comparing candidates from the list with the type of self on the methods. The reference calls these "method with a receiver of that type". They are talking about the type of self for each method.

The dot operand and the self type for each method considered are two distinct things!

The reference could/should be more clear here, by not using the unqualified word "receiver" to mean multiple things. And they're also sloppy when talking about T's implementations. The implementing type does not matter -- it doesn't influence the results -- only the type of self on the method matters.[1]


I found this hard to follow, so let me try another tact instead. What are the visible methods called clone that are somewhat related[2] to our &Box<T> and so on? I'll assume T: Clone.

They are:

Implementing type (and return type) Type of self: &Self in Clone::clone
T &T
&T &&T
&&T &&&T
Box<T> &Box<T>
&Box<T> &&Box<T>
&&Box<T> &&&Box<T>

And now let's walk through the four examples, comparing types in our candidate list to the type of self on the method -- types in the second column above.

let bx: &Box<T> = todo();  bx.clone();
// Candidate list: 
//   &Box<T> &&Box<T> &mut &Box<T>
//   Box<T> &Box<T> &mut Box<T>
//   T &T &mut T

Try the candidates in order.

  • Is there a &Box<T> in the second column above?
    • Yes, it matches <Box<T> as Clone>::clone and the return type is Box<T>.
    • Notional desugaring: <Box<T> as Clone>::clone(bx)
let bx: &mut Box<T> = todo();  bx.clone();
// Candidate list: 
//   &mut Box<T> &&mut Box<T> &mut &mut Box<T>
//   Box<T> &Box<T> &mut Box<T>
//   T &T &mut T

Try the candidates in order.

  • Is there a &mut Box<T> in the second column above? No.
  • Is there a &&mut Box<T> in the second column above? No.
  • Is there a &mut &mut Box<T> in the second column above? No.
  • Is there a Box<T> in the second column above? No.
  • Is there a &Box<T> in the second column above?
    • Yes, it matches <Box<T> as Clone>::clone and the return type is Box<T>.
    • Notional desugaring: <Box<T> as Clone>::clone(&*bx)
let bx: Box<T> = todo();  bx.clone();
// Candidate list: 
//   Box<T> &Box<T> &mut Box<T>
//   T &T &mut T

Try the candidates in order.

  • Is there a Box<T> in the second column above? No.
  • Is there a &Box<T> in the second column above?
    • Yes, it matches <Box<T> as Clone>::clone and the return type is Box<T>.
    • Notional desugaring: <Box<T> as Clone>::clone(&bx)
let bx: &&Box<T> = todo();  bx.clone();
// Candidate list: 
//   &&Box<T> &&&Box<T> &mut &&Box<T>
//   &Box<T> &&Box<T> &mut &Box<T>
//   Box<T> &Box<T> &mut Box<T>
//   T &T &mut T

Try the candidates in order.

  • Is there a &&Box<T> in the second column above?
    • Yes, it matches <&Box<T> as Clone>::clone and the return type is &Box<T>. Oops!
    • Notional desugaring: <&Box<T> as Clone>::clone(bx)

I don't know if this is part of the confusion or not, but what sort of derefencing, auto-referencing, and coercions are applied is decided by the results of the search. The notional desugaring is decided after a match has been found. The whole search process and notional desugaring happens as part of compilation, not at run time.


  1. It will be impossible for the type of self to match anything in the candidate list for some implementors, but that doesn't change what the algorithm does. ↩︎

  2. I could have made a list of all implementations, but the forum disallows infinitely long posts :wink: ↩︎

2 Likes

Yes, I think it is much more clear now; I will re-read earlier replies with the new interpretation.

I could have made a list of all implementations, but the forum disallows infinitely long posts :wink:

It would have been reassuring :)