Skip to content

Commit 9e35d5e

Browse files
committed
change to AsBorrowRef version
1 parent af7dc42 commit 9e35d5e

File tree

1 file changed

+145
-58
lines changed

1 file changed

+145
-58
lines changed

text/0000-entry-into-owned.md

Lines changed: 145 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
[summary]: #summary
88

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

1414
```rust
1515
let string_map: HashMap<String, u64> = ...;
@@ -27,7 +27,8 @@ 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).
3132

3233
# Motivation
3334
[motivation]: #motivation
@@ -80,8 +81,6 @@ Specifically we're looking for a fix which supports the following cases
8081
# Detailed design
8182
[design]: #detailed-design
8283

83-
[Playground Proof of Concept](https://is.gd/0lpGej)
84-
8584
## Approach
8685
To justify the approach taken by this proposal, first consider the following
8786
(unworkable) solution:
@@ -94,75 +93,92 @@ To justify the approach taken by this proposal, first consider the following
9493
```
9594

9695
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:
96+
implementation requires `Clone`. To work around this limitation we define a
97+
different trait `std::borrow::AsBorrowOf`:
9998

10099
```rust
101-
pub trait IntoOwned<T> {
100+
pub trait AsBorrowOf<T, B: ?Sized>: Sized where T: Borrow<B> {
102101
fn into_owned(self) -> T;
102+
fn as_borrow_of(&self) -> &B;
103103
}
104104

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

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

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

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-
```
119+
1. There is a by-value conversion `Self` -> `T`.
120+
2. Both `T` and `Self` can be borrowed as `&B`.
121+
122+
These properties are precisely what we need an `entry` query: we need (2) to
123+
hash and/or compare the query against exiting keys in the map and we need (1) to
124+
convert the query into a key on vacant insertion.
125+
126+
The two impl-s capture that
123127

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

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

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

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

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.
144159

145160
# Drawbacks
146161
[drawbacks]: #drawbacks
147162

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.
163+
1. The docs of `entry` get uglier and introduce a new trait the user
164+
never needs to manually implement.
154165

155166
2. It does not offer a way of recovering a `!Clone` key when no `insert`
156167
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.
168+
of different ways eg. an `into_query` method on `Entry`.
169+
170+
4. The changes to `entry` would be insta-stable (not the new traits). There's
171+
no real way of feature-gating this.
159172

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.
173+
5. May break inference for uses of maps where `entry` is the only call (`K` can
174+
no longer be necessarily inferred as the arugment of `entry`). May also hit
175+
issue [#37138](https://github.com/rust-lang/rust/issues/37138).
163176

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

167183
# Alternatives
168184
[alternatives]: #alternatives
@@ -177,16 +193,87 @@ get-like calls.
177193

178194
3. Pro: Solves the recovery of `!Clone` keys.
179195

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

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-
```
278+
1. Are the backwards compatibility hazards acceptable?
279+
2. Is the `IntoOwned` version preferable?

0 commit comments

Comments
 (0)