Skip to content

Closure-consuming helper functions for Debug{Struct,List,Set,Tuple} #288

Closed
@jmillikin

Description

@jmillikin

Proposal

Problem statement

Implementing fmt::Debug for FFI types can be difficult because their layouts often don't match Rust conventions, for example by using a bitset for boolean properties or a (tag, union) pair instead of an enumeration.

Currently the formatting of these values can require creating auxiliary wrapper types that exist just to impl Debug for one particular field, which is verbose and unergonomic.

Motivating examples or use cases

Example one: a bitset representing a set of boolean options. The bits should be formatted with names, and unknown bits should be formatted as numbers.

#[derive(Eq, PartialEq)]
struct FancyBitset(u32);

impl FancyBitset {
    const A: FancyBitset = FancyBitset(1 << 0);
    const B: FancyBitset = FancyBitset(1 << 1);
    const C: FancyBitset = FancyBitset(1 << 2);
    const D: FancyBitset = FancyBitset(1 << 3);
    const E: FancyBitset = FancyBitset(1 << 4);
}

impl fmt::Debug for FancyBitset {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str("FancyBitset ")?;
        let mut dbg = f.debug_set();
        for bit in 0..32 {
            let mask: u32 = 1 << bit;
            if self.0 & mask == 0 {
                continue;
            }
            dbg.entry(&DebugFancyBit(mask));
        }
        dbg.finish()
    }
}

// This wrapper type shouldn't be necessary
struct DebugFancyBit(u32);
impl fmt::Debug for DebugFancyBit {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match FancyBitset(self.0) {
            FancyBitset::A => f.write_str("A"),
            FancyBitset::B => f.write_str("B"),
            FancyBitset::C => f.write_str("C"),
            FancyBitset::D => f.write_str("D"),
            FancyBitset::E => f.write_str("E"),
            _ => write!(f, "{:#010X}", self.0),
        }
    }
}

Example two: a union field conceptually only has one of its sub-fields populated at any given time, but figuring out which sub-field is valid requires inspecting the tag.

struct StructWithUnion {
    tag: u32, // 0 = u32, 1 = i32, 2 = f32
    union_value: TheUnion
}

union TheUnion {
    as_u32: u32,
    as_i32: i32,
    as_f32: f32,
}

impl fmt::Debug for StructWithUnion {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_struct("StructWithUnion")
            .field("tag", &self.tag)
            .field("value", &DebugTheUnion(self.tag, &self.union_value))
            .finish()
    }
}

// This wrapper type shouldn't be necessary
struct DebugTheUnion<'a>(u32, &'a TheUnion);
impl fmt::Debug for DebugTheUnion<'_> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let mut dbg = f.debug_struct("TheUnion");
        match self.0 {
            0 => dbg.field("as_u32", unsafe { &self.1.as_u32 }),
            1 => dbg.field("as_i32", unsafe { &self.1.as_i32 }),
            2 => dbg.field("as_f32", unsafe { &self.1.as_f32 }),
            _ => dbg.field("", &"<unknown tag>"),
        };
        dbg.finish()
    }
}

Solution sketch

Add methods to DebugStruct, DebugList, DebugSet, and DebugTuple that make it easy to embed a formatting closure function directly into the main impl Debug of a type.

I'm ignoring DebugMap, but if the libs team thinks it should have similar functionality then i suggest key_with() and value_with() rather than trying to have a double-closure entry_with().

Also, I'm not married to the names, so you'd prefer field_fmt() or whatever then that's fine.

impl DebugStruct<'_, '_> {
    fn field_with<F>(&mut self, name: &str, fmt_fn: F) -> &mut Self
    where
        F: FnOnce(&mut Formatter) -> Result;
}

impl DebugList<'_, '_> {
    fn entry_with<F>(&mut self, fmt_fn: F) -> &mut Self
    where
        F: FnOnce(&mut fmt::Formatter) -> fmt::Result;
}

impl DebugSet<'_, '_> {
    fn entry_with<F>(&mut self, fmt_fn: F) -> &mut Self
    where
        F: FnOnce(&mut fmt::Formatter) -> fmt::Result;
}

impl DebugTuple<'_, '_> {  // also DebugSet
    fn field_with<F>(&mut self, fmt_fn: F) -> &mut Self
    where
        F: FnOnce(&mut fmt::Formatter) -> fmt::Result;
}

They would be used like this:

impl fmt::Debug for FancyBitset {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str("FancyBitset ")?;
        let mut dbg = f.debug_set();
        for bit in 0..32 {
            let mask: u32 = 1 << bit;
            if self.0 & mask == 0 {
                continue;
            }
            dbg.entry_with(|f| match FancyBitset(mask) {
                FancyBitset::A => f.write_str("A"),
                FancyBitset::B => f.write_str("B"),
                FancyBitset::C => f.write_str("C"),
                FancyBitset::D => f.write_str("D"),
                FancyBitset::E => f.write_str("E"),
                _ => write!(f, "{:#010X}", mask),
            });
        }
        dbg.finish()
    }
}

impl fmt::Debug for StructWithUnion {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_struct("StructWithUnion")
            .field("tag", &self.tag)
            .field_with("value", |f| {
                let mut u = f.debug_struct("TheUnion");
                match self.tag {
                    0 => u.field("as_u32", unsafe { &self.union_value.as_u32 }),
                    1 => u.field("as_i32", unsafe { &self.union_value.as_i32 }),
                    2 => u.field("as_f32", unsafe { &self.union_value.as_f32 }),
                    _ => u.field("", &"<unknown tag>"),
                };
                u.finish()
            })
            .finish()
    }
}

Alternatives

Closure-wrapper function

This is more generic because it lets a closure be used in any place a &dyn Debug is expected, but it's slightly less flexible (F: Fn rather than F: FnOnce) and the call site is more verbose due to required type annotations.

struct DebugFn<F>(F);

impl<F> Debug for DebugFn<F>
where
    F: Fn(&mut Formatter) -> Result,
{
    fn fmt(&self, f: &mut Formatter) -> Result {
        (self.0)(f)
    }
}

impl fmt::Debug for StructWithUnion {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_struct("SomeStruct")
            .field("value", &fmt::DebugFn(|f: &mut fmt::Formatter| match self.tag {
                // ...
            }))
            .finish()
    }
}

Extension trait

Mostly I just wanted to see if it was possible. It is, but it's horrible. trait Debug quite reasonably uses &self receivers, so the only way to get FnOnce is to play games with a Cell<Option<F>>

trait DebugStructWith {
    fn field_with<F>(&mut self, name: &str, fmt_fn: F) -> &mut Self
    where
        F: FnOnce(&mut fmt::Formatter) -> fmt::Result;
}

impl DebugStructWith for fmt::DebugStruct<'_, '_> {
    fn field_with<F>(&mut self, name: &str, fmt_fn: F) -> &mut Self
    where
        F: FnOnce(&mut fmt::Formatter) -> fmt::Result,
    {
        struct DebugFn<F>(Cell<Option<F>>);

        impl<F> fmt::Debug for DebugFn<F>
            where F: FnOnce(&mut fmt::Formatter) -> fmt::Result,
        {
            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
                let fmt_fn = self.0.replace(None).unwrap();
                fmt_fn(f)
            }
        }
    
        self.field(name, &DebugFn(Cell::new(Some(fmt_fn))))
    }
}

Links and related work

What happens now?

This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.

Possible responses

The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):

  • We think this problem seems worth solving, and the standard library might be the right place to solve it.
  • We think that this probably doesn't belong in the standard library.

Second, if there's a concrete solution:

  • We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
  • We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ACP-acceptedAPI Change Proposal is accepted (seconded with no objections)T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions