From 50132d98d3c33caab3ab98fdbead551f85453984 Mon Sep 17 00:00:00 2001 From: thirdsgames Date: Thu, 15 Jul 2021 21:51:37 +0100 Subject: [PATCH 1/6] Add cloning example for dot operator behaviour Signed-off-by: thirdsgames --- src/dot-operator.md | 59 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/dot-operator.md b/src/dot-operator.md index a1fc33bd..6ebff27e 100644 --- a/src/dot-operator.md +++ b/src/dot-operator.md @@ -4,3 +4,62 @@ The dot operator will perform a lot of magic to convert types. It will perform auto-referencing, auto-dereferencing, and coercion until types match. TODO: steal information from http://stackoverflow.com/questions/28519997/what-are-rusts-exact-auto-dereferencing-rules/28552082#28552082 + +Consider the following example of the dot operator at work. +```rust.ignore +fn do_stuff(value: &T) { + let cloned = value.clone(); +} +``` +What type is `cloned`? First, the compiler checks if we can call by value. +The type of `value` is `&T`, and so the `clone` function has signature +`fn clone(&T) -> T`. We know that `T: Clone`, so the compiler finds that +`cloned: T`. + +What would happen if the `T: Clone` restriction was removed? We would not be able +to call by value, since there is no implementation of `Clone` for `T`. So the +compiler tries to call by autoref. In this case, the function has signature +`fn clone(&&T) -> &T` since `Self = &T`. The compiler sees that `&T: Clone`, and +then deduces that `cloned: &T`. + +Here is another example where the autoref behaviour is used to create some subtle +effects. +```rust.ignore +use std::sync::Arc; + +#[derive(Clone)] +struct Container(Arc); + +fn clone_containers(foo: &Container, bar: &Container) { + let foo_cloned = foo.clone(); + let bar_cloned = bar.clone(); +} +``` +What types are `foo_cloned` and `bar_cloned`? We know that `Container: Clone`, +so the compiler calls `clone` by value to give `foo_cloned: Container`. +However, `bar_cloned` actually has type `&Container`. Surely this doesn't make +sense - we added `#[derive(Clone)]` to `Container`, so it must implement `Clone`! +Looking closer, the code generated by the `derive` macro is (roughly) +```rust.ignore +impl Clone for Container where T: Clone { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } +} +``` +The derived `Clone` implementation is +[only defined where `T: Clone`](https://doc.rust-lang.org/std/clone/trait.Clone.html#derivable), +so there is no implementation for `Container: Clone` for a generic `T`. The +compiler then looks to see if `&Container` implements `Clone`, which it does. +So it deduces that `clone` is called by autoref, and so `bar_cloned` has type +`&Container`. + +We can fix this by implementing `Clone` manually without requiring `T: Clone`. +```rust.ignore +impl Clone for Container { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } +} +``` +Now, the type checker deduces that `bar_cloned: Container`. From d829c51589b22283b415eb607ba8c147f04985d4 Mon Sep 17 00:00:00 2001 From: thirdsgames Date: Thu, 15 Jul 2021 22:21:50 +0100 Subject: [PATCH 2/6] Outline method lookup semantics Signed-off-by: thirdsgames --- src/dot-operator.md | 46 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/dot-operator.md b/src/dot-operator.md index 6ebff27e..b84e55c8 100644 --- a/src/dot-operator.md +++ b/src/dot-operator.md @@ -2,10 +2,52 @@ The dot operator will perform a lot of magic to convert types. It will perform auto-referencing, auto-dereferencing, and coercion until types match. +The detailed mechanics of method lookup are defined [here](https://rustc-dev-guide.rust-lang.org/method-lookup.html), +but here is a brief overview that outlines the main steps. -TODO: steal information from http://stackoverflow.com/questions/28519997/what-are-rusts-exact-auto-dereferencing-rules/28552082#28552082 +Suppose we have a function `foo` that has a receiver (a `self`, `&self` or +`&mut self` parameter). If we call `value.foo()`, the compiler needs to determine +what type `Self` is before it can call the correct implementation of the function. +For this example, we will say that `value` has type `T`. -Consider the following example of the dot operator at work. +We will use [fully-qualified syntax](https://doc.rust-lang.org/nightly/book/ch19-03-advanced-traits.html#fully-qualified-syntax-for-disambiguation-calling-methods-with-the-same-name) +to be more clear about exactly which type we are calling a function on. + +- First, the compiler checks if we can call `T::foo(value)` directly. +This is called a "by value" method call. +- If we can't call this function (for example, if the function has the wrong type +or a trait isn't implemented for `Self`), then the compiler tries to add in an +automatic reference. This means that the compiler tries `<&T>::foo(value)` and +`<&mut T>::foo(value)`. This is called an "autoref" method call. +- If none of these candidates worked, we dereference `T` and try again. This +uses the `Deref` trait - if `T: Deref` then we try again with type `U` +instead of `T`. If we can't dereference `T`, we can also try _unsizing_ `T`. +This just means that if `T` has a size parameter known at compile time, we "forget" +it for the purpose of resolving methods. For instance, this unsizing step can +convert `[i32; 2]` into `[i32]` by "forgetting" the size of the array. + +Here is an example of the method lookup algorithm. +```rust.ignore +let array: Rc> = ...; +let first_entry = array[0]; +``` + +How does the compiler actually compute `array[0]` when the array is behind so +many indirections? First, `array[0]` is really just syntax sugar for the [`Index`](https://doc.rust-lang.org/std/ops/trait.Index.html) +trait - the compiler will convert `array[0]` into `array.index(0)`. Now, the +compiler checks to see if `array` implements `Index`, so that we can call the +function. + +First, the compiler checks if `Rc>` implements `Index`, but it +does not, and neither do `&Rc>` or `&mut Rc>`. Since +none of these worked, the compiler dereferences the `Rc>` into +`Box<[T; 3]>` and tries again. `Box<[T; 3]>`, `&Box<[T; 3]>` and `&mut Box<[T; 3]>` +do not implement `Index`, so it dereferences again. `[T; 3]` and its autorefs +also do not implement `Index`. We can't dereference `[T; 3]`, so the compiler +unsizes it, giving `[T]`. Finally, `[T]` implements `Index`, so we can now call the +actual `index` function. + +Consider the following more complicated example of the dot operator at work. ```rust.ignore fn do_stuff(value: &T) { let cloned = value.clone(); From f4653e1234c8a63843dd073aad078f3be93dbccb Mon Sep 17 00:00:00 2001 From: thirdsgames Date: Thu, 15 Jul 2021 22:23:58 +0100 Subject: [PATCH 3/6] Move links to bottom Signed-off-by: thirdsgames --- src/dot-operator.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/dot-operator.md b/src/dot-operator.md index b84e55c8..751eef69 100644 --- a/src/dot-operator.md +++ b/src/dot-operator.md @@ -2,7 +2,7 @@ The dot operator will perform a lot of magic to convert types. It will perform auto-referencing, auto-dereferencing, and coercion until types match. -The detailed mechanics of method lookup are defined [here](https://rustc-dev-guide.rust-lang.org/method-lookup.html), +The detailed mechanics of method lookup are defined [here][method_lookup], but here is a brief overview that outlines the main steps. Suppose we have a function `foo` that has a receiver (a `self`, `&self` or @@ -10,7 +10,7 @@ Suppose we have a function `foo` that has a receiver (a `self`, `&self` or what type `Self` is before it can call the correct implementation of the function. For this example, we will say that `value` has type `T`. -We will use [fully-qualified syntax](https://doc.rust-lang.org/nightly/book/ch19-03-advanced-traits.html#fully-qualified-syntax-for-disambiguation-calling-methods-with-the-same-name) +We will use [fully-qualified syntax][fqs] to be more clear about exactly which type we are calling a function on. - First, the compiler checks if we can call `T::foo(value)` directly. @@ -33,7 +33,7 @@ let first_entry = array[0]; ``` How does the compiler actually compute `array[0]` when the array is behind so -many indirections? First, `array[0]` is really just syntax sugar for the [`Index`](https://doc.rust-lang.org/std/ops/trait.Index.html) +many indirections? First, `array[0]` is really just syntax sugar for the [`Index`][index] trait - the compiler will convert `array[0]` into `array.index(0)`. Now, the compiler checks to see if `array` implements `Index`, so that we can call the function. @@ -90,7 +90,7 @@ impl Clone for Container where T: Clone { } ``` The derived `Clone` implementation is -[only defined where `T: Clone`](https://doc.rust-lang.org/std/clone/trait.Clone.html#derivable), +[only defined where `T: Clone`][clone], so there is no implementation for `Container: Clone` for a generic `T`. The compiler then looks to see if `&Container` implements `Clone`, which it does. So it deduces that `clone` is called by autoref, and so `bar_cloned` has type @@ -105,3 +105,8 @@ impl Clone for Container { } ``` Now, the type checker deduces that `bar_cloned: Container`. + +[fqs]: https://doc.rust-lang.org/nightly/book/ch19-03-advanced-traits.html#fully-qualified-syntax-for-disambiguation-calling-methods-with-the-same-name +[method_lookup]: https://rustc-dev-guide.rust-lang.org/method-lookup.html +[index]: https://doc.rust-lang.org/std/ops/trait.Index.html +[clone]: https://doc.rust-lang.org/std/clone/trait.Clone.html#derivable From d0a45b752c09cb61abac114a82c2028cf01068da Mon Sep 17 00:00:00 2001 From: Thirds <50671761+thirdsgames@users.noreply.github.com> Date: Fri, 16 Jul 2021 09:23:28 +0100 Subject: [PATCH 4/6] Apply suggestions from code review Co-authored-by: Yuki Okushi --- src/dot-operator.md | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/dot-operator.md b/src/dot-operator.md index 751eef69..a3c2701f 100644 --- a/src/dot-operator.md +++ b/src/dot-operator.md @@ -26,8 +26,9 @@ This just means that if `T` has a size parameter known at compile time, we "forg it for the purpose of resolving methods. For instance, this unsizing step can convert `[i32; 2]` into `[i32]` by "forgetting" the size of the array. -Here is an example of the method lookup algorithm. -```rust.ignore +Here is an example of the method lookup algorithm: + +```rust,ignore let array: Rc> = ...; let first_entry = array[0]; ``` @@ -38,21 +39,23 @@ trait - the compiler will convert `array[0]` into `array.index(0)`. Now, the compiler checks to see if `array` implements `Index`, so that we can call the function. -First, the compiler checks if `Rc>` implements `Index`, but it +Then, the compiler checks if `Rc>` implements `Index`, but it does not, and neither do `&Rc>` or `&mut Rc>`. Since none of these worked, the compiler dereferences the `Rc>` into -`Box<[T; 3]>` and tries again. `Box<[T; 3]>`, `&Box<[T; 3]>` and `&mut Box<[T; 3]>` +`Box<[T; 3]>` and tries again. `Box<[T; 3]>`, `&Box<[T; 3]>`, and `&mut Box<[T; 3]>` do not implement `Index`, so it dereferences again. `[T; 3]` and its autorefs also do not implement `Index`. We can't dereference `[T; 3]`, so the compiler unsizes it, giving `[T]`. Finally, `[T]` implements `Index`, so we can now call the actual `index` function. -Consider the following more complicated example of the dot operator at work. -```rust.ignore +Consider the following more complicated example of the dot operator at work: + +```rust fn do_stuff(value: &T) { let cloned = value.clone(); } ``` + What type is `cloned`? First, the compiler checks if we can call by value. The type of `value` is `&T`, and so the `clone` function has signature `fn clone(&T) -> T`. We know that `T: Clone`, so the compiler finds that @@ -60,15 +63,16 @@ The type of `value` is `&T`, and so the `clone` function has signature What would happen if the `T: Clone` restriction was removed? We would not be able to call by value, since there is no implementation of `Clone` for `T`. So the -compiler tries to call by autoref. In this case, the function has signature +compiler tries to call by autoref. In this case, the function has the signature `fn clone(&&T) -> &T` since `Self = &T`. The compiler sees that `&T: Clone`, and then deduces that `cloned: &T`. -Here is another example where the autoref behaviour is used to create some subtle -effects. -```rust.ignore -use std::sync::Arc; +Here is another example where the autoref behavior is used to create some subtle +effects: +```rust +# use std::sync::Arc; +# #[derive(Clone)] struct Container(Arc); @@ -77,11 +81,13 @@ fn clone_containers(foo: &Container, bar: &Container) { let bar_cloned = bar.clone(); } ``` + What types are `foo_cloned` and `bar_cloned`? We know that `Container: Clone`, so the compiler calls `clone` by value to give `foo_cloned: Container`. However, `bar_cloned` actually has type `&Container`. Surely this doesn't make sense - we added `#[derive(Clone)]` to `Container`, so it must implement `Clone`! -Looking closer, the code generated by the `derive` macro is (roughly) +Looking closer, the code generated by the `derive` macro is (roughly): + ```rust.ignore impl Clone for Container where T: Clone { fn clone(&self) -> Self { @@ -89,6 +95,7 @@ impl Clone for Container where T: Clone { } } ``` + The derived `Clone` implementation is [only defined where `T: Clone`][clone], so there is no implementation for `Container: Clone` for a generic `T`. The @@ -96,17 +103,19 @@ compiler then looks to see if `&Container` implements `Clone`, which it does. So it deduces that `clone` is called by autoref, and so `bar_cloned` has type `&Container`. -We can fix this by implementing `Clone` manually without requiring `T: Clone`. -```rust.ignore +We can fix this by implementing `Clone` manually without requiring `T: Clone`: + +```rust,ignore impl Clone for Container { fn clone(&self) -> Self { Self(Arc::clone(&self.0)) } } ``` + Now, the type checker deduces that `bar_cloned: Container`. -[fqs]: https://doc.rust-lang.org/nightly/book/ch19-03-advanced-traits.html#fully-qualified-syntax-for-disambiguation-calling-methods-with-the-same-name +[fqs]: ../book/ch19-03-advanced-traits.html#fully-qualified-syntax-for-disambiguation-calling-methods-with-the-same-name [method_lookup]: https://rustc-dev-guide.rust-lang.org/method-lookup.html -[index]: https://doc.rust-lang.org/std/ops/trait.Index.html -[clone]: https://doc.rust-lang.org/std/clone/trait.Clone.html#derivable +[index]: ../std/ops/trait.Index.html +[clone]: ../std/clone/trait.Clone.html#derivable From f9a9c9fb606c3539a658aee2ea87976e08a315d0 Mon Sep 17 00:00:00 2001 From: Thirds <50671761+thirdsgames@users.noreply.github.com> Date: Fri, 16 Jul 2021 09:26:04 +0100 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Yuki Okushi --- src/dot-operator.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dot-operator.md b/src/dot-operator.md index a3c2701f..f04f5663 100644 --- a/src/dot-operator.md +++ b/src/dot-operator.md @@ -88,7 +88,7 @@ However, `bar_cloned` actually has type `&Container`. Surely this doesn't mak sense - we added `#[derive(Clone)]` to `Container`, so it must implement `Clone`! Looking closer, the code generated by the `derive` macro is (roughly): -```rust.ignore +```rust,ignore impl Clone for Container where T: Clone { fn clone(&self) -> Self { Self(Arc::clone(&self.0)) From ad27976f8fc4a9d5fccb2421f9f20dd9e094afba Mon Sep 17 00:00:00 2001 From: thirdsgames Date: Fri, 16 Jul 2021 09:31:55 +0100 Subject: [PATCH 6/6] One sentence per line; "we" => "compiler/it" Signed-off-by: thirdsgames --- src/dot-operator.md | 96 +++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 43 deletions(-) diff --git a/src/dot-operator.md b/src/dot-operator.md index f04f5663..16fbf007 100644 --- a/src/dot-operator.md +++ b/src/dot-operator.md @@ -1,30 +1,35 @@ # The Dot Operator -The dot operator will perform a lot of magic to convert types. It will perform -auto-referencing, auto-dereferencing, and coercion until types match. +The dot operator will perform a lot of magic to convert types. +It will perform auto-referencing, auto-dereferencing, and coercion until types +match. The detailed mechanics of method lookup are defined [here][method_lookup], but here is a brief overview that outlines the main steps. Suppose we have a function `foo` that has a receiver (a `self`, `&self` or -`&mut self` parameter). If we call `value.foo()`, the compiler needs to determine -what type `Self` is before it can call the correct implementation of the function. +`&mut self` parameter). +If we call `value.foo()`, the compiler needs to determine what type `Self` is before +it can call the correct implementation of the function. For this example, we will say that `value` has type `T`. -We will use [fully-qualified syntax][fqs] -to be more clear about exactly which type we are calling a function on. +We will use [fully-qualified syntax][fqs] to be more clear about exactly which +type we are calling a function on. -- First, the compiler checks if we can call `T::foo(value)` directly. +- First, the compiler checks if it can call `T::foo(value)` directly. This is called a "by value" method call. -- If we can't call this function (for example, if the function has the wrong type +- If it can't call this function (for example, if the function has the wrong type or a trait isn't implemented for `Self`), then the compiler tries to add in an -automatic reference. This means that the compiler tries `<&T>::foo(value)` and -`<&mut T>::foo(value)`. This is called an "autoref" method call. -- If none of these candidates worked, we dereference `T` and try again. This -uses the `Deref` trait - if `T: Deref` then we try again with type `U` -instead of `T`. If we can't dereference `T`, we can also try _unsizing_ `T`. -This just means that if `T` has a size parameter known at compile time, we "forget" -it for the purpose of resolving methods. For instance, this unsizing step can -convert `[i32; 2]` into `[i32]` by "forgetting" the size of the array. +automatic reference. +This means that the compiler tries `<&T>::foo(value)` and `<&mut T>::foo(value)`. +This is called an "autoref" method call. +- If none of these candidates worked, it dereferences `T` and tries again. +This uses the `Deref` trait - if `T: Deref` then it tries again with +type `U` instead of `T`. +If it can't dereference `T`, it can also try _unsizing_ `T`. +This just means that if `T` has a size parameter known at compile time, it "forgets" +it for the purpose of resolving methods. +For instance, this unsizing step can convert `[i32; 2]` into `[i32]` by "forgetting" +the size of the array. Here is an example of the method lookup algorithm: @@ -34,19 +39,21 @@ let first_entry = array[0]; ``` How does the compiler actually compute `array[0]` when the array is behind so -many indirections? First, `array[0]` is really just syntax sugar for the [`Index`][index] -trait - the compiler will convert `array[0]` into `array.index(0)`. Now, the -compiler checks to see if `array` implements `Index`, so that we can call the -function. +many indirections? +First, `array[0]` is really just syntax sugar for the [`Index`][index] trait - +the compiler will convert `array[0]` into `array.index(0)`. +Now, the compiler checks to see if `array` implements `Index`, so that it can call +the function. Then, the compiler checks if `Rc>` implements `Index`, but it -does not, and neither do `&Rc>` or `&mut Rc>`. Since -none of these worked, the compiler dereferences the `Rc>` into -`Box<[T; 3]>` and tries again. `Box<[T; 3]>`, `&Box<[T; 3]>`, and `&mut Box<[T; 3]>` -do not implement `Index`, so it dereferences again. `[T; 3]` and its autorefs -also do not implement `Index`. We can't dereference `[T; 3]`, so the compiler -unsizes it, giving `[T]`. Finally, `[T]` implements `Index`, so we can now call the -actual `index` function. +does not, and neither do `&Rc>` or `&mut Rc>`. +Since none of these worked, the compiler dereferences the `Rc>` into +`Box<[T; 3]>` and tries again. +`Box<[T; 3]>`, `&Box<[T; 3]>`, and `&mut Box<[T; 3]>` do not implement `Index`, +so it dereferences again. +`[T; 3]` and its autorefs also do not implement `Index`. +It can't dereference `[T; 3]`, so the compiler unsizes it, giving `[T]`. +Finally, `[T]` implements `Index`, so it can now call the actual `index` function. Consider the following more complicated example of the dot operator at work: @@ -56,16 +63,18 @@ fn do_stuff(value: &T) { } ``` -What type is `cloned`? First, the compiler checks if we can call by value. +What type is `cloned`? +First, the compiler checks if it can call by value. The type of `value` is `&T`, and so the `clone` function has signature -`fn clone(&T) -> T`. We know that `T: Clone`, so the compiler finds that -`cloned: T`. +`fn clone(&T) -> T`. +It knows that `T: Clone`, so the compiler finds that `cloned: T`. -What would happen if the `T: Clone` restriction was removed? We would not be able -to call by value, since there is no implementation of `Clone` for `T`. So the -compiler tries to call by autoref. In this case, the function has the signature -`fn clone(&&T) -> &T` since `Self = &T`. The compiler sees that `&T: Clone`, and -then deduces that `cloned: &T`. +What would happen if the `T: Clone` restriction was removed? It would not be able +to call by value, since there is no implementation of `Clone` for `T`. +So the compiler tries to call by autoref. +In this case, the function has the signature `fn clone(&&T) -> &T` since +`Self = &T`. +The compiler sees that `&T: Clone`, and then deduces that `cloned: &T`. Here is another example where the autoref behavior is used to create some subtle effects: @@ -82,10 +91,12 @@ fn clone_containers(foo: &Container, bar: &Container) { } ``` -What types are `foo_cloned` and `bar_cloned`? We know that `Container: Clone`, -so the compiler calls `clone` by value to give `foo_cloned: Container`. -However, `bar_cloned` actually has type `&Container`. Surely this doesn't make -sense - we added `#[derive(Clone)]` to `Container`, so it must implement `Clone`! +What types are `foo_cloned` and `bar_cloned`? +We know that `Container: Clone`, so the compiler calls `clone` by value to give +`foo_cloned: Container`. +However, `bar_cloned` actually has type `&Container`. +Surely this doesn't make sense - we added `#[derive(Clone)]` to `Container`, so it +must implement `Clone`! Looking closer, the code generated by the `derive` macro is (roughly): ```rust,ignore @@ -96,10 +107,9 @@ impl Clone for Container where T: Clone { } ``` -The derived `Clone` implementation is -[only defined where `T: Clone`][clone], -so there is no implementation for `Container: Clone` for a generic `T`. The -compiler then looks to see if `&Container` implements `Clone`, which it does. +The derived `Clone` implementation is [only defined where `T: Clone`][clone], +so there is no implementation for `Container: Clone` for a generic `T`. +The compiler then looks to see if `&Container` implements `Clone`, which it does. So it deduces that `clone` is called by autoref, and so `bar_cloned` has type `&Container`.