[Pre-RFC] Maximum depth for pretty-printed debug {:#?}

Pretty-printing is great at enhancing the readability of debug output, but expanding too much can also make it hard to read.

Consider the case where I want to pretty-print an array of ordered pairs, Vec<(usize, usize)>. In this case, each entry will take 4 lines of output -

[
    (
        19,
        -3,
    ),
    (
        13,
        -2,
    ),
    (
        15,
        -2,
    ),
    (
        17,
        -2,
    ),
    (
        19,
        -2,
    ),
    ...
]

- when printing each entry on a single line would be sufficient for readability -

[
    (19, -3),
    (13, -2),
    (15, -2),
    (17, -2),
    (19, -2),
    ...
]

- and arguably more readable, especially for bigger lists.

What I propose to fix this is allowing the user to specify a maximum pretty-printing depth in the format string. For a max depth of n, only the n-topmost-level compound types will be expanded. In this case, the desired output can be achieved by setting a maximum depth of 1.

4 Likes

The formatting micro language is already quite complex. How would the alternate format depth argument be provided? Would it allow dollar-variable assignment?

1 Like

The problem in general is not about depth, and can't be solved well with maximum depth.

If you had the small tuple at shallower depth, you wouldn't want it displayed in multiple lines either. Imagine a case of a tree structure where every node has one field with a small tuple (you want it compact), and other node recursing deeper into the tree (you want it multiline). There is no depth value that would print it well.

So the problem is in recursing in debug fmt and knowing which parts of the structure are going to be "small" and which are "large".

9 Likes

Sounds like what is needed is a way to enumerate paths to exclude/include for full expansion. Something similar to XPath or CSS selectors. I doubt though that would be a satisfactory solution.

Probably attributes on the members of structures/tuples/enums for use by the derive debug macro would work better.?

An approach that didn't care as much about (re) allocations could be written to take into account the size of the current node's children before formatting it, but I don't know people would be happy with us changing that to be the default. This could be solved in a more reasonable way if the formatting machinery was pluggable and replaceable by crates.

Maybe this could be solved by letting Debug types report a heuristic length similar to Iterator's size_hint(), but even less precise than that, so that implementations could return a fixed guesstimate without running actual formatting.

Debug output of a tuple could then ask its children how long their output may be, and based on that choose compact or multiline layout.

It will all be guesstimated, but it's a cosmetic issue, so it doesn't have to be correct every time.

1 Like

That size hint should be wired to Arguments::estimated_capacity if it gets introduced.

Another option is just to provide another method on Debug like is_short, that defaults to false. If true the pretty-printer will inline.

is_short on its own doesn't really work with nesting, e.g. (5, 6) is short, so is (5, (6, 7)), but if you keep nesting two-tuples of integers you'll eventually hit something that is "not short". (Though maybe this could be tracked in the type system via specialization).

I was recently playing around with a similar usecase (formatting JSON like output for user display) and ended up going with a trait like:

trait LengthEstimate {
    /// Can shortcircuit and return `max` if it is more than that
    fn estimate(&self, max: usize) -> usize;
}

being able to shortcircuit to avoid recursing into deeply nested types or long arrays is also useful, e.g. for an array I do:

let mut len = 4;
for item in &self {
    len += item.estimate(max - len) + 2;
    if len >= max {
        return len;
    }
}
len
1 Like

Yeah I think you are right - I just thought I'd make the is_short suggesting because of its simplicity.

Another option to avoid runtime cost of recursion would be to make the hint a const fn.

We don't have const trait functions yet (not even on nightly)