Skip to content

Commit 158eaea

Browse files
authored
RFC 2696
1 parent 60b973a commit 158eaea

File tree

1 file changed

+259
-0
lines changed

1 file changed

+259
-0
lines changed

text/2696-debug-map-key-value.md

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
- Feature Name: `debug_map_key_value`
2+
- Start Date: 2019-05-01
3+
- RFC PR: [rust-lang/rfcs#2696](https://github.com/rust-lang/rfcs/pull/2696)
4+
- Rust Issue: [rust-lang/rust#62482](https://github.com/rust-lang/rust/issues/62482)
5+
6+
# Summary
7+
[summary]: #summary
8+
9+
Add two new methods to `std::fmt::DebugMap` for writing the key and value part of a map entry separately:
10+
11+
```rust
12+
impl<'a, 'b: 'a> DebugMap<'a, 'b> {
13+
pub fn key(&mut self, key: &dyn Debug) -> &mut Self;
14+
pub fn value(&mut self, value: &dyn Debug) -> &mut Self;
15+
}
16+
```
17+
18+
# Motivation
19+
[motivation]: #motivation
20+
21+
The format builders available to `std::fmt::Debug` implementations through the `std::fmt::Formatter` help keep the textual debug representation of Rust structures consistent. They're also convenient to use and make sure the various formatting flags are retained when formatting entries. The standard formatting API in `std::fmt` is similar to `serde::ser`:
22+
23+
- `Debug` -> `Serialize`
24+
- `Formatter` -> `Serializer`
25+
- `DebugMap` -> `SerializeMap`
26+
- `DebugList` -> `SerializeSeq`
27+
- `DebugTuple` -> `SerializeTuple` / `SerializeTupleStruct` / `SerilizeTupleVariant`
28+
- `DebugStruct` -> `SerializeStruct` / `SerializeStructVariant`
29+
30+
There's one notable inconsistency though: an implementation of `SerializeMap` must support serializing its keys and values independently. This isn't supported by `DebugMap` because its `entry` method takes both a key and a value together. That means it's not possible to write a `Serializer` that defers entirely to the format builders.
31+
32+
Adding separate `key` and `value` methods to `DebugMap` will align it more closely with `SerializeMap`, and make it possible to build a `Serializer` based on the standard format builders.
33+
34+
# Guide-level explanation
35+
[guide-level-explanation]: #guide-level-explanation
36+
37+
In `DebugMap`, an entry is the pair of a key and a value. That means the following `Debug` implementation:
38+
39+
```rust
40+
use std::fmt;
41+
42+
struct Map;
43+
44+
impl fmt::Debug for Map {
45+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
46+
let mut map = f.debug_map();
47+
48+
map.entry(&"key", &"value");
49+
50+
map.finish()
51+
}
52+
}
53+
```
54+
55+
is equivalent to:
56+
57+
```rust
58+
impl fmt::Debug for Map {
59+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
60+
let mut map = f.debug_map();
61+
62+
// Equivalent to map.entry
63+
map.key(&"key").value(&"value");
64+
65+
map.finish()
66+
}
67+
}
68+
```
69+
70+
Every call to `key` must be directly followed by a corresponding call to `value` to complete the entry:
71+
72+
```rust
73+
impl fmt::Debug for Map {
74+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
75+
let mut map = f.debug_map();
76+
77+
map.key(&1);
78+
79+
// err: attempt to start a new entry without finishing the current one
80+
map.key(&2);
81+
82+
map.finish()
83+
}
84+
}
85+
```
86+
87+
`key` must be called before `value`:
88+
89+
```rust
90+
impl fmt::Debug for Map {
91+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
92+
let mut map = f.debug_map();
93+
94+
// err: attempt to write a value without first writing its key
95+
map.value(&"value");
96+
map.key(&"key");
97+
98+
map.finish()
99+
}
100+
}
101+
```
102+
103+
Each entry must be finished before the map can be finished:
104+
105+
```rust
106+
impl fmt::Debug for Map {
107+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
108+
let mut map = f.debug_map();
109+
110+
map.key(&1);
111+
112+
// err: attempt to finish a map that has an incomplete key
113+
map.finish()
114+
}
115+
}
116+
```
117+
118+
Any incorrect calls to `key` and `value` will panic.
119+
120+
## When to use `key` and `value`
121+
122+
Why would you want to use `key` and `value` directly if they're less convenient than `entry`? The reason is when the driver of the `DebugMap` is a framework like `serde` rather than a data structure directly:
123+
124+
```rust
125+
struct DebugMap<'a, 'b: 'a>(fmt::DebugMap<'a, 'b>);
126+
127+
impl<'a, 'b: 'a> SerializeMap for DebugMap<'a, 'b> {
128+
type Ok = ();
129+
type Error = Error;
130+
131+
fn serialize_key<T: ?Sized>(&mut self, key: &T) -> Result<(), Self::Error>
132+
where
133+
T: Serialize,
134+
{
135+
self.0.key(&key.to_debug());
136+
Ok(())
137+
}
138+
139+
fn serialize_value<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error>
140+
where
141+
T: Serialize,
142+
{
143+
self.0.value(&value.to_debug());
144+
Ok(())
145+
}
146+
147+
fn serialize_entry<K: ?Sized, V: ?Sized>(
148+
&mut self,
149+
key: &K,
150+
value: &V,
151+
) -> Result<(), Self::Error>
152+
where
153+
K: Serialize,
154+
V: Serialize,
155+
{
156+
self.0.entry(&key.to_debug(), &value.to_debug());
157+
Ok(())
158+
}
159+
160+
fn end(self) -> Result<Self::Ok, Self::Error> {
161+
self.0.finish().map_err(Into::into)
162+
}
163+
}
164+
```
165+
166+
Consumers should prefer calling `entry` over `key` and `value`.
167+
168+
# Reference-level explanation
169+
[reference-level-explanation]: #reference-level-explanation
170+
171+
The `key` and `value` methods can be implemented on `DebugMap` by tracking the state of the current entry in a `bool`, and splitting the existing `entry` method into two:
172+
173+
```rust
174+
pub struct DebugMap<'a, 'b: 'a> {
175+
has_key: bool,
176+
..
177+
}
178+
179+
pub fn debug_map_new<'a, 'b>(fmt: &'a mut fmt::Formatter<'b>) -> DebugMap<'a, 'b> {
180+
DebugMap {
181+
has_key: false,
182+
..
183+
}
184+
}
185+
186+
impl<'a, 'b: 'a> DebugMap<'a, 'b> {
187+
pub fn entry(&mut self, key: &dyn fmt::Debug, value: &dyn fmt::Debug) -> &mut DebugMap<'a, 'b> {
188+
self.key(key).value(value)
189+
}
190+
191+
pub fn key(&mut self, key: &dyn fmt::Debug) -> &mut DebugMap<'a, 'b> {
192+
// Make sure there isn't a partial entry
193+
assert!(!self.has_key, "attempted to begin a new map entry without completing the previous one");
194+
195+
self.result = self.result.and_then(|_| {
196+
// write the key
197+
198+
// Mark that we're in an entry
199+
self.has_key = true;
200+
Ok(())
201+
});
202+
203+
self
204+
}
205+
206+
pub fn value(&mut self, value: &dyn fmt::Debug) -> &mut DebugMap<'a, 'b> {
207+
// Make sure there is a partial entry to finish
208+
assert!(self.has_key, "attempted to format a map value before its key");
209+
210+
self.result = self.result.and_then(|_| {
211+
// write the value
212+
213+
// Mark that we're not in an entry
214+
self.has_key = false;
215+
Ok(())
216+
});
217+
218+
self.has_fields = true;
219+
self
220+
}
221+
222+
pub fn finish(&mut self) -> fmt::Result {
223+
// Make sure there isn't a partial entry
224+
assert!(!self.has_key, "attempted to finish a map with a partial entry");
225+
226+
self.result.and_then(|_| self.fmt.write_str("}"))
227+
}
228+
}
229+
```
230+
231+
# Drawbacks
232+
[drawbacks]: #drawbacks
233+
234+
The proposed `key` and `value` methods are't immediately useful for `Debug` implementors that are able to call `entry` instead. This creates a decision point where there wasn't one before. The proposed implementation is also going to be less efficient than the one that exists now because it introduces a few conditionals.
235+
236+
On balance, the additional `key` and `value` methods are a small and unsurprising addition that enables a set of use-cases that weren't possible before, and aligns more closely with `serde`.
237+
238+
# Rationale and alternatives
239+
[rationale-and-alternatives]: #rationale-and-alternatives
240+
241+
The universal alternative of simply _not doing this_ leaves consumers that do need to format map keys independently of values with a few options:
242+
243+
- Write an alternative implementation of the format builders. The output from this alternative implementation would need to be kept reasonably in-sync with the one in the standard library. It doesn't change very frequently, but does from time to time. It would also have to take the same care as the standard library implementation to retain formatting flags when working with entries.
244+
- Buffer keys and format them together with values when the whole entry is available. Unless the key is guaranteed to live until the value is supplied (meaning it probably needs to be `'static`) then the key will need to be formatted into a string first. This means allocating (though the cost could be amortized over the whole map) and potentially losing formatting flags when buffering.
245+
246+
Another alternative is to avoid panicking if the sequence of entries doesn't follow the expected pattern of `key` then `value`. Instead, `DebugMap` could make a best-effort attempt to represent keys without values and values without keys. However, this approach has the drawback of masking incorrect `Debug` implementations, may produce a surprising output and doesn't reduce the complexity of the implementation (we'd still need to tell whether a key should be followed by a `: ` separator or a `, `).
247+
248+
# Prior art
249+
[prior-art]: #prior-art
250+
251+
The `serde::ser::SerializeMap` API (and `libserialize::Encoder` for what it's worth) requires map keys and values can be serialized independently. `SerializeMap` provides a `serialize_entry` method, which is similar to the existing `DebugMap::entry`, but is only supposed to be used as an optimization.
252+
253+
# Unresolved questions
254+
[unresolved-questions]: #unresolved-questions
255+
256+
# Future possibilities
257+
[future-possibilities]: #future-possibilities
258+
259+
The internal implementation could optimize the `entry` method to avoid a few redundant checks.

0 commit comments

Comments
 (0)