Obviously you get a stack overflow for structures having #[derive(Debug)]
when you try to debug print out them. Is there some standard solutions to prevent looping? I am asking that to save some time on own implementation.
You can override the Debug
implementation:
use std::fmt;
// #[derive(Debug)]
struct Struct {
n: i32,
s: &'static Struct,
}
impl fmt::Debug for Struct {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Struct")
.field("n", &self.n)
// use format_args! to prevent quotes around the placeholder
.field("s", &format_args!("<omitted to prevent an infinite loop>"))
.finish()
}
}
static S: Struct = Struct { s: &S, n: 42 };
fn main() {
println!("{S:?}");
}
Great. So it will be a case specific solution. Let me try it. Maybe someone will propose something generic.
Probably you don’t want the " "
around the placeholder, so that could use
.field("s", &format_args!("<omitted to prevent an infinite loop>"))
One generic approach for making specific cases easier can be to use rust-analyzer to help. It offers functionality to essentially “convert derive(Something…)
into a manual impl”; so then you only have to adapt/change it but don’t need to build up the whole impl
from the ground yourself.
Realistically, “self referencing structures” is too broad a category to consider fully generic solutions… many cases will be of the form that simply always omitting one particular field is a good approach. (In some cases you can also perhaps represent the field in some custom way that still gives information, e.g. about which object is pointed to[1], without becoming recursive.)
The most complex of cases might be arbitrarily complex graph-like structures. You could solve this with various custom approaches, ranging from “not solving the problem at all” and just not printing much at all, over approaches of tracking (or trying to track) already-visited items somehow on the side to detect cycles dynamically, to fully customized representations of your data structure.
especially if there is some natural object “identiy” already present anyway ↩︎
Here's a JS implementation I simplified from some existing internal tooling I've done earlier, it also handles arbitrary graph sharing, e.g. shows a value is the same instance in non-cyclic graphs.
Output:
#1={
first: 1,
second: 2,
self: #1,
other: #2={
target: #1,
nested: [
#2,
"simple value",
],
},
twice: #3={
},
again: #3,
simple: {
no: "ids",
needed: "here",
},
}
From the implementation:
const target = { first: 1, second: 2 };
target.self = target;
target.other = { target };
target.other.nested = [target.other, "simple value"];
target.twice = {};
target.again = target.twice;
target.simple = { no: "ids", needed: "here" };
function print_cyclic(root, printer) {
const seen = new Set([root]);
// first pass: find shared
const shared = new Set();
for (const obj of seen) {
for (const value of Object.values(obj)) {
if (seen.has(value)) {
shared.add(value);
} else {
seen.add(value);
}
}
}
// second pass: print
let last_id = 0;
let opened_shared_ids = new Map();
print(root);
function print(value) {
if (!value || typeof value !== "object") {
printer.value(value);
return;
}
if (shared.has(value)) {
let id = opened_shared_ids.get(value);
if (id) {
printer.repeat(id);
return;
}
id = ++last_id;
opened_shared_ids.set(value, id);
printer.start_define(id);
}
if (Array.isArray(value)) {
printer.start_array();
for (const item of value) {
print(item);
printer.end_item();
}
printer.end_array();
} else {
printer.start_object();
for (const [name, item] of Object.entries(value)) {
printer.start_member(name);
print(item);
printer.end_item();
}
printer.end_object();
}
}
}
class Printer {
indent = 0
output = process.stdout
at_start_of_line = true
write(text) {
let end = 0;
while (end < text.length) {
const start = end;
end = text.indexOf("\n", start) + 1;
if (this.at_start_of_line) {
this.output.write(" ".repeat(this.indent));
}
if (end == 0) {
this.output.write(text.slice(start));
this.at_start_of_line = false;
break;
}
this.output.write(text.slice(start, end));
this.at_start_of_line = true;
}
}
repeat(id) {
this.write(`#${id}`);
}
start_define(id) {
this.write(`#${id}=`);
}
start_member(name) {
this.write(`${name}: `);
}
end_item() {
this.write(",\n");
}
value(value) {
this.write(JSON.stringify(value));
}
start_array() {
this.write("[\n");
this.indent += 2;
}
end_array() {
this.indent -= 2;
this.write("]");
}
start_object() {
this.write("{\n");
this.indent += 2;
}
end_object() {
this.indent -= 2;
this.write("}");
}
}
print_cyclic(target, new Printer());
There's a few quirks to consider translating this to Rust - you'll need a representation of a node id you can compare and copy or clone so you can put it in the various sets and maps, you'll want a way to delegate Debug
to an implementation like this, and finding a way to derive the field visitor would be extremely nice.
derive
is not magical, when you derive Debug
on a struct, there's no other possible implementation than recursively calling Debug::fmt()
on each field.
this works fine for tree-like data structures (for graph shaped data structures, as long as they are acyclic, infinite recursion is not possible, although the data might not be formatted accurately).
however, if your data structure has some kind of cycles, you'll have to manually implement Debug
to break the cycle. the derive macro simply cannot handle every cases.
I became a very lazy switching to Rust. I also handle myself a situation with cyclic dependencies in a generic in Java. But Rust teaches us - do nothing, Rust will take care, but sometimes - oops....
I have found mostly that it yells at you when you're doing something dumb; but that is mostly local reasoning. It's generally at least hard to create cyclic object graphs accidentally?
Not sure about accidentally, but on a purpose. Since we are comparing to other languages approach, I mostly can confirm that Java somehow manages cycling references in toString().
Yeah, node/browser console logging too (though very differently)