Skip to content

Commit 740c4f4

Browse files
committed
change to AsBorrowRef version
1 parent af7dc42 commit 740c4f4

File tree

1 file changed

+144
-56
lines changed

1 file changed

+144
-56
lines changed

text/0000-entry-into-owned.md

Lines changed: 144 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
Enable the map Entry API to take borrowed keys as arguments, cloning only when
1010
necessary. The proposed implementation introduces a new trait
11-
`std::borrow::IntoOwned` which enables the existing `entry` methods to accept
11+
`std::borrow::AsBorrowOf` which enables the existing `entry` methods to accept
1212
borrows. In effect, it makes the following possible:
1313

1414
```rust
@@ -27,7 +27,9 @@ borrows. In effect, it makes the following possible:
2727
*nonclone_map.entry(NonCloneable::new()).or_insert(0) += 1; // Can't and doesn't clone.
2828
```
2929

30-
See [playground](https://is.gd/0lpGej) for a concrete demonstration.
30+
See [playground](https://is.gd/XBVgDe) and [prototype
31+
implementation](https://github.com/rust-lang/rust/pull/37143) for a concrete
32+
demonstration.
3133

3234
# Motivation
3335
[motivation]: #motivation
@@ -80,8 +82,6 @@ Specifically we're looking for a fix which supports the following cases
8082
# Detailed design
8183
[design]: #detailed-design
8284

83-
[Playground Proof of Concept](https://is.gd/0lpGej)
84-
8585
## Approach
8686
To justify the approach taken by this proposal, first consider the following
8787
(unworkable) solution:
@@ -94,75 +94,92 @@ To justify the approach taken by this proposal, first consider the following
9494
```
9595

9696
This would support (2) and (3) but not (1) because `ToOwned`'s blanket
97-
implementation requires `Clone`. To work around this limitation we take a trick
98-
out of `IntoIterator`'s book and add a new `std::borrow::IntoOwned` trait:
97+
implementation requires `Clone`. To work around this limitation we define a
98+
different trait `std::borrow::AsBorrowOf`:
9999

100100
```rust
101-
pub trait IntoOwned<T> {
101+
pub trait AsBorrowOf<T, B: ?Sized>: Sized where T: Borrow<B> {
102102
fn into_owned(self) -> T;
103+
fn as_borrow_of(&self) -> &B;
103104
}
104105

105-
impl<T> IntoOwned<T> for T {
106-
default fn into_owned(self) -> T { self }
106+
impl<T> AsBorrowOf<T, T> for T {
107+
fn into_owned(self) -> T { self }
108+
fn as_borrow_of(&self) -> &Self { self }
107109
}
108110

109-
impl<T: RefIntoOwned> IntoOwned<T::Owned> for T {
110-
default fn into_owned(self) -> T::Owned { self.ref_into_owned() }
111+
impl<'a, B: ToOwned + ?Sized> AsBorrowOf<B::Owned, B> for &'a B {
112+
fn into_owned(self) -> B::Owned { self.to_owned() }
113+
fn as_borrow_of(&self) -> &B { *self }
111114
}
115+
```
112116

113-
trait RefIntoOwned {
114-
type Owned: Sized;
115-
fn ref_into_owned(self) -> Self::Owned;
116-
}
117+
This trait defines a relationship between three types `T`, `B` and `Self` with
118+
the following properties:
117119

118-
impl<'a, T: ?Sized + ToOwned> RefIntoOwned for &'a T {
119-
type Owned = <T as ToOwned>::Owned;
120-
fn ref_into_owned(self) -> T::Owned { (*self).to_owned() }
121-
}
122-
```
120+
1. There is a by-value conversion `Self` -> `T`.
121+
2. Both `T` and `Self` can be borrowed as `&B`.
122+
123+
These properties are precisely what we need an `entry` query: we need (2) to
124+
hash and/or compare the query against exiting keys in the map and we need (1) to
125+
convert the query into a key on vacant insertion.
126+
127+
The two impl-s capture that
123128

124-
The auxilliary `RefIntoOwned` trait is needed to avoid the coherence issues
125-
which an
129+
1. `T` can always be converted to `T` and borrowed as `&T`. This enables
130+
by-value keys.
131+
2. `&B` can be converted to `B::Owned` and borrowed as `&B`, when B:
132+
`ToOwned`. This enables borrows of `Clone` types.
133+
134+
Then we modify the `entry` signature (for `HashMap`, but similar for `BTreeMap`)
135+
to
126136

127137
```rust
128-
impl<'a, T: ?Sized + ToOwned> IntoOwned<T::Owned> for &'a T {
129-
fn into_owned(self) -> T::Owned { (*self).to_owned() }
138+
pub fn entry<'a, Q, B>(&'a self, k: Q) -> Entry<'a, K, V, Q>
139+
where Q: AsBorrowOf<K, B>
140+
K: Borrow<B>,
141+
B: Hash + Eq {
142+
// use `hash(key.as_borrow_of())` and `key.as_borrow_of() == existing_key.borrow()`
143+
// for comparisions and `key.into_owned()` on `VacantEntry::insert`.
130144
}
131145
```
132146

133-
implementation would cause. Then we modify the `entry` signature to
147+
## Detailed changes:
134148

135-
```rust
136-
pub fn entry<'a, Q>(&'a self, k: Q) -> Entry<'a, K, V, Q>
137-
where Q: Hash + Eq + IntoOwned<K>
138-
```
149+
Also see [working implementation](https://github.com/rust-lang/rust/pull/37143)
150+
for diff.
151+
152+
1. Add `std::borrow::Borrow` as described in previous section.
153+
2. Change `Entry` to add a `Q` type parameter defaulted to `K` for backwards
154+
compatibility (for `HashMap` and `BTreeMap`).
155+
3. `Entry::key`, `VacantEntry::key` and `VacantEntry::into_key` are moved to a
156+
separate `impl` block to be implemented only for the `Q=K` case.
157+
4. `Entry::or_insert`, `Entry::or_insert_with` and `VacantEntry::insert` gain
158+
a `B` type parameter and appropriate constraints: `where Q: AsBorrowOf<K, B>, K: Borrow<B>, B: Hash + Eq`.
139159

140-
and add a new `Q: IntoOwned<K>` type parameter to `Entry`. This can be done
141-
backwards-compatibly with a `Q=K` default. The new `Entry` type will store
142-
`key: Q` and call `into_owned` on insert-like calls, while using `Q` directly on
143-
get-like calls.
144160

145161
# Drawbacks
146162
[drawbacks]: #drawbacks
147163

148-
1. The docs of `entry` get uglier and introduce two new traits the user
149-
never needs to manually implement. If there was support for `where A != B`
150-
clauses we could get rid of the `RefIntoOwned` trait, but that would still
151-
leave `IntoOwned` (which is strictly more general than the existing `ToOwned`
152-
trait). On the other hand `IntoOwned` may be independently useful in generic
153-
contexts.
164+
1. The docs of `entry` get uglier and introduce a new trait the user
165+
never needs to manually implement.
154166

155167
2. It does not offer a way of recovering a `!Clone` key when no `insert`
156168
happens. This is somewhat orthogonal though and could be solved in a number
157-
of different ways eg. an `into_key` method on `Entry` or via an `IntoOwned`
158-
impl on a `&mut Option<T>`-like type.
169+
of different ways eg. an `into_query` method on `Entry`.
170+
171+
4. The changes to `entry` would be insta-stable (not the new traits). There's
172+
no real way of feature-gating this.
159173

160-
3. Further depend on specialisation in its current form for a public API. If the
161-
exact parameters of specialisation change, and this particular pattern
162-
doesn't work anymore, we'll have painted ourselves into a corner.
174+
5. May break inference for uses of maps where `entry` is the only call (`K` can
175+
no longer be necessarily inferred as the arugment of `entry`). May also hit
176+
issue [#37138](https://github.com/rust-lang/rust/issues/37138).
163177

164-
4. The implementation would be insta-stable. There's no real way of
165-
feature-gating this.
178+
6. The additional `B` type parameter on `on_insert_with` is a backwards
179+
compatibility hazard, since it breaks explicit type parameters
180+
(e.g. `on_insert_with::<F>` would need to become `on_insert_with::<F, _>`).
181+
This seems very unlikely to happen in practice: F is almost always a closure
182+
and even when it isn't `on_insert_with` can always infer the type of `F`.
166183

167184
# Alternatives
168185
[alternatives]: #alternatives
@@ -177,16 +194,87 @@ get-like calls.
177194

178195
3. Pro: Solves the recovery of `!Clone` keys.
179196

197+
2. Add a `entry_or_clone` with an `Q: Into<Cow<K>>` bound.
198+
199+
1. Con: Adds a new method as well as new `Entry` types for all maps.
200+
201+
2. Con: Passes on the problem to any generic users of maps with every layer
202+
of abstraction needing to provide an `or_clone` variant.
203+
204+
3. Pro: probably clearest backwards compatible solution, doesn't introduce
205+
any new traits.
206+
207+
3. Split `AsBorrowOf` into `AsBorrowOf` and `IntoOwned`. This is closer to the
208+
original proposal:
209+
210+
1. Con: Requires introducing three new traits.
211+
212+
2. Con: Requires specialisation to implement a public API, tying us closer
213+
to current parameters of specialisation.
214+
215+
3. Pro: `IntoOwned` may be independently useful as a more general
216+
`ToOwned`.
217+
218+
4. Pro: no additional `B` type parameter on `on_insert` and
219+
`on_insert_with`.
220+
221+
Code:
222+
```rust
223+
pub trait IntoOwned<T> {
224+
fn into_owned(self) -> T;
225+
}
226+
227+
impl<T> IntoOwned<T> for T {
228+
default fn into_owned(self) -> Self {
229+
self
230+
}
231+
}
232+
233+
impl<T> IntoOwned<T::Owned> for T
234+
where T: RefIntoOwned
235+
{
236+
default fn into_owned(self) -> T::Owned {
237+
self.ref_into_owned()
238+
}
239+
}
240+
241+
pub trait AsBorrowOf<T, B: ?Sized>: IntoOwned<T> where T: Borrow<B> {
242+
fn as_borrow_of(&self) -> &B;
243+
}
244+
245+
impl<T> AsBorrowOf<T, T> for T {
246+
default fn as_borrow_of(&self) -> &Self {
247+
self
248+
}
249+
}
250+
251+
impl<'a, B: ToOwned + ?Sized> AsBorrowOf<B::Owned, B> for &'a B {
252+
default fn as_borrow_of(&self) -> &B {
253+
*self
254+
}
255+
}
256+
257+
// Auxilliary trait to get around coherence issues.
258+
pub trait RefIntoOwned {
259+
type Owned: Sized;
260+
261+
fn ref_into_owned(self) -> Self::Owned;
262+
}
263+
264+
impl<'a, T: ?Sized> RefIntoOwned for &'a T
265+
where T: ToOwned
266+
{
267+
type Owned = <T as ToOwned>::Owned;
268+
269+
fn ref_into_owned(self) -> T::Owned {
270+
(*self).to_owned()
271+
}
272+
}
273+
274+
```
275+
180276
# Unresolved questions
181277
[unresolved]: #unresolved-questions
182278

183-
1. Should these traits ever be stabilised? `RefIntoOwned` in particular can go
184-
away with the inclusion of `where A != B` clauses:
185-
186-
```rust
187-
impl<'a, T: ?Sized + ToOwned> IntoOwned<T::Owned> for &'a T
188-
where T::Owned != &'a T
189-
{
190-
fn into_owned(self) -> T::Owned { (*self).to_owned() }
191-
}
192-
```
279+
1. Are the backwards compatibility hazards acceptable?
280+
2. Is the `IntoOwned` version preferable?

0 commit comments

Comments
 (0)