TL;DR
We’re looking to stabilize the debug builder APIs in the near future, so it’s time to make sure we’re happy with the functionality. Speak now or forever hold your peace.
Background
The debug builder APIs are part of the implementation of RFC 640: Debug Improvements. It proposed that the #
“alternate” flag would be treated by fmt::Debug
implementations as a request to “pretty print” output - turning
{ "foo": ComplexType { thing: Some(BufferedReader { reader: FileStream { path: "/home/sfackler/rust/README.md", mode: R }, buffer: 1013/65536 }), other_thing: 100 }, "bar": ComplexType { thing: Some(BufferedReader { reader: FileStream { path: "/tmp/foobar", mode: R }, buffer: 0/65536 }), other_thing: 0 } }
into
{
"foo": ComplexType {
thing: Some(
BufferedReader {
reader: FileStream {
path: "/home/sfackler/rust/README.md",
mode: R
},
buffer: 1013/65536
}
),
other_thing: 100
},
"bar": ComplexType {
thing: Some(
BufferedReader {
reader: FileStream {
path: "/tmp/foobar",
mode: R
},
buffer: 0/65536
}
),
other_thing: 0
}
}
The information content is the same - the only changes present are newlines and whitespace.
It would be a huge pain to have to manually handle both output variants via manual write!
calls in fmt::Debug
implementations, so some builder-style types were added to handle the complications automatically. The builders are currently used by #[derive(Debug)]
but are unstable so manual Debug
implementations can’t use them in stable code.
The API
The entry points consist of a series of methods on the fmt::Formatter
struct:
impl<'a> Formatter {
pub fn debug_struct<'b>(&'b mut self, name: &str) -> DebugStruct<'b, 'a> { ... }
pub fn debug_tuple<'b>(&'b mut self, name: &str) -> DebugTuple<'b, 'a> { ... }
pub fn debug_list<'b>(&'b mut self) -> DebugList<'b, 'a> { ... }
pub fn debug_set<'b>(&'b mut self) -> DebugSet<'b, 'a> { ... }
pub fn debug_map<'b>(&'b mut self) -> DebugMap<'b, 'a> { ... }
}
Each of these types defines a builder-style API:
impl<'a, 'b> DebugStruct<'a, 'b> {
pub fn field(self, name: &str, value: &fmt::Debug) -> DebugStruct<'a, 'b> { ... }
pub fn finish(self) -> fmt::Result { ... }
}
impl<'a, 'b> DebugTuple<'a, 'b> {
pub fn field(self, value: &fmt::Debug) -> DebugTuple<'a, 'b> { ... }
pub fn finish(self) -> fmt::Result { ... }
}
impl<'a, 'b> DebugList<'a, 'b> {
pub fn entry(self, value: &fmt::Debug) -> DebugList<'a, 'b> { ... }
pub fn finish(self) -> fmt::Result { ... }
}
impl<'a, 'b> DebugSet<'a, 'b> {
pub fn entry(self, value: &fmt::Debug) -> DebugSet<'a, 'b> { ... }
pub fn finish(self) -> fmt::Result { ... }
}
impl<'a, 'b> DebugMap<'a, 'b> {
pub fn entry(self, key: &fmt::Debug, value: &fmt::Debug) -> DebugMap<'a, 'b> { ... }
pub fn finish(self) -> fmt::Result { ... }
}
Each variant corresponds to a common output format:
-
DebugStruct
:MyType { field1: value1, field2: value2 }
-
DeubgTuple
:MyType(value1, value2)
-
DebugList
:[value1, value2]
-
DebugSet
:{value1, value2}
-
DebugMap
:{key1: value1, key2: value2}
Common usage in a Debug
implementation would typically look something like
impl fmt::Debug for MyType {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.debug_struct("MyType")
.field("field1", &self.field1)
.field("field2", &self.field2)
.finish()
}
}
Stabilization
An open PR proposes to stabilize these APIs as-is. Is there anything that we should change before that happens? Potential points of discussion that have been mentioned before:
- Should we use
debug_tuple_struct
andDebugTupleStruct
instead ofdebug_tuple
andDebugTuple
? It’s technically more correct but adds verbosity. -
DebugStruct
andDebugTuple
have afield
method whileDebugList
,DebugSet
andDebugMap
have anentry
method. Should they be unified? It’s inconsistent but does match to what we normally refer to the contents of those kinds of structures by. - Should the builder methods take
&mut self
instead ofself
? Taking self by value makes the API a bit more foolproof (you can’t add more fields/entries after callingfinish
for example) but does mean you have to reassign the builder when using it in more complex cases:
let mut builder = fmt.debug_struct("MyType");
if some_condition {
builder = builder.field("conditional_field", &self.field);
// vs simply
// builder.field("conditional_field", &self.field);
}
builder.finish()
On the other hand, it allows for a nice one liner when implementing Debug
for common data structures:
impl<K, V, S> Debug for HashMap<K, V, S>
where K: Eq + Hash + Debug, V: Debug, S: HashState
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.iter().fold(f.debug_map(), |b, (k, v)| b.entry(k, v)).finish()
}
}
Thoughts?