I've made another update to my `trace.d` module.

The changes are:
- switched to `camelCase` from `snake_case` to match most of the D ecosystem - added `traceFunc` for the common case of using `traceScope` on a function's scope - changed some of the functions to use `format` in their implementations - narrowed the `public import` statements to not muddle the user's namespace as much - changed doc comments to multi-line comments and split the text into multiple lines instead of relying on long lines being wrapped in the user's text editor - added `in` qualifiers to all input for slight "const correctness" clarity improvement

There's some subjectivity and slight pros and cons to these changes of course.

Anyway, here is the new code for `trace.d`:

```D

public import std.stdio: writeln, writefln;
public import std.conv: to;

import std.format;

/**
`tracePrefix` provides the `moduleName.functionName-lineNumber:\t`
prefix that is added to each of the tracing functions in this module.

The other functions in this module are likely to be more useful, unless you want to reuse this tracing prefix to create a new tracing function.

The `shouldMark` parameter is for preventing confusion in `traceScope`.
**/
string tracePrefix(in bool shouldMark = false) {
        return format(
                `__FUNCTION__ ~ "-" ~ to!string(__LINE__) ~ %s ~ ": \t"`,
                (shouldMark ? `"*"` : `""`)
        );
}

/**
`mixin(traceMessage(message))` outputs whatever arbitrary string you pass to it, but prefixed by what module, function, and line number the `traceMessage` call is located on, thus aiding print-based debugging.
**/
string traceMessage(in string message) {
        return format(
                `writeln(%s ~ q"<%s>");`,
                tracePrefix(), message
        );
}

/**
`traceScope` returns a mixin string that if mixed in at the beginning of a scope inserts code that logs when the current scope starts and ends, which may be useful for debugging potentially, since it informs of when you are actually inside that scope or not, which may or may not be when you think. Use it by writing `mixin(traceScope);`, placing it
at the top of the scope you intend to trace.
**/
string traceScope(in string scopeName) {
        return format(
                `writeln(%s ~ q"<%s>" ~ " entered");` ~
                `scope(exit) writeln(%s ~ q"<%s>" ~ " exited");`,
                tracePrefix(), scopeName,
                tracePrefix(true), scopeName
        );
}

/**
`traceFunc` is shorthand for `traceScope(__FUNCTION__)`, since that is one of the most common use cases for `traceScope`. The user must
remember to only use `traceFunc` at the top of a function's scope
though, because otherwise if `traceFunc` is placed inside some other
scope within that function then the output will be misleading.
**/
string traceFunc(in string scopeName = __FUNCTION__) {
        return traceScope(scopeName);
}

/**
`show` eases print debugging by printing both an expression as a string and its corresponding evaluated value. It is intended to be used via `mixin`, such as in `mixin(show("1 + 2")`, which will print something like `module.function-line: 1 + 2 == 3`. The `module.function-line:` prefix helps distinguish debug printing from normal output and also
helps to track where output is coming from.

Printing both an expression's code and the expression's value makes print debugging easier to keep track of accurately (especially when
multiple print statements are used, which would otherwise be more
easily confused with each other) and less redundant (since `show` being a macro prevents the user from needing to write the expression twice).

However, assertions (via `assert`) and unit tests (via `unittest`) are often a better way to debug and maintain code than any form of print debugging, because (1) assertions are enforced automatically instead of requiring manual inspection and (2) reading through print statements
consumes a lot of time in aggregate whereas assertions are
instantaneous and hence much more expedient when well applicable.
Nonetheless, print debugging is still very useful and intuitive,
especially when you don't know what a value is at all.
**/
string show(in string expr) {
return `writefln(` ~ tracePrefix() ~ `~ "%s == %s", q"<` ~ expr ~ `>", ` ~ expr ~ `);`;
}

/*
`debug(logName)` can be prefixed to traces that you only want to show sometimes, but hardcoding that into this module would probably be a bad idea. It seems better to leave it to the user's discretion to decide
whether they want each trace function output to show up in regular
output or only in a debug logging mode, though debugging is the intent.
*/
```

Here's a premade `main.d` file for testing it, for your convenience:

```D
import std.stdio;

import trace;

void main() {
  mixin(traceFunc);

  mixin(show("1 + 2"));

  immutable int x = 1;
  mixin(show("x"));

  mixin(traceMessage("This is a generic trace message..."));

writeln("This is a normal (non-trace) print statement just before the end of `main`.");
}
```

Do you guys think the use of `format` improves the readability of the code or makes it worse? I'm not entirely sure. I am also still using `~` concatenation for the `show` case because that one was giving me trouble still for trying to make it more readable.

Also, I hear D has string interpolation but with an [odd implementation](https://dlang.org/phobos/core_interpolation.html) that makes me wary of using it and I wonder if I should try to use those kinds of strings here or not.

I have also located some related discussions that you may find of value or interest: - ["Convenient debug printing" thread on D forum from ~2019](https://forum.dlang.org/post/svjjawiezudnugdyr...@forum.dlang.org) - ["Tracing D Application" official D blog post by Alexandr Druzhinin from ~2020](https://dlang.org/blog/2020/03/13/tracing-d-applications/) - ["Debugging improvements - Visual Studio Natvis, GDB, LLDB" D forum post from ~2021](https://forum.dlang.org/post/rgboxhmtyurmgcvjf...@forum.dlang.org) and the corresponding [GitHub repo](https://github.com/Pure-D/dlang-debug) for it... This in contrast requires a debugger to be active though and has a vast multitude more dependencies than my above provided simple tracing code.

Are there preexisting community libraries for this that aren't a mess of dependencies?

It still strikes me as odd that this isn't in the standard library since it is one of the most universally useful things that can exist in any programming language. It's ironic that only a handful of languages support it.

This also strikes me as being proof of the value of macros and indeed I suspect that languages without strong metaprogramming are likely an evolutionary dead end in the long term given how the `show` macro and tracing functions like these are so clearly universally useful and pragmatically essential for expedient coding.

On the other hand though, all languages have subtle pros and cons like this and features that are or aren't in the standard facilities, etc. D has several such features missing from almost all others, such as its slice operations and nuanced template distinctions.

Relatedly, one thing that I previously felt more negative about but have realized has more upside than I previously thought is D's requirement to use `mixin` whenever text macros are used. Doing so makes it easier to find locations in a code base that are being expanded by macro magic to do more than meets the eye, which can make navigating and understanding code easier. It makes it so that the most "magic" parts of the code are marked as such. I've not encountered any other language that does that as well.

D is a very practical language overall and I really appreciate the careful thought and dedicated effort that has gone into it. It's nice to have a language that feels like more sane C++ and D does the best job overall of that. It really should be more widely used.

Reply via email to