Local Variables as Accidental Breadcrumbs for Faster Debugging
bugsink.com
bugsink.com
My personal answer is yes, absolutely.
15 years ago I wrote a blog post "Local variables are free": https://lapcatsoftware.com/blog/2009/12/19/local-variables-a...
Updated 7 years ago for Swift: https://lapcatsoftware.com/articles/local-variables-are-stil...
Edited to add: and thanks for keeping it up to date with the new Swift version!
(I have a lot of frustration around modern software taking a ton of CPU power to do almost nothing, but local variables aren't to blame for that.)
You can now do this in Jetbrains products. Pretty awesome, you can even step through them.
if i == 6 {
println!(“xxx”); // breakpoint set here in my ide
}
The breakpoint always fires, and unlike an assert I can run the program forward from there to see why i == 6 (or whatever) is problematic.In JavaScript / typescript you can also just write “debugger;” in the code and it’ll break on that line when a debugger is attached. I wish more languages supported that.
But nowadays I've realized many benefits. It often simplifies dealing with line-wrapping and debugging; but more importantly, such a local variable is a name for a subexpression, and giving such a name helps document the code. Making the reader step through the code forces awareness of what the steps are and why they need to exist. And yes, naming things is one of the "hard problems", but that's not a reason to avoid it.
Yes, and many programming languages have assertions such as "assert greater than or equal to".
For example with Rust and the Assertables crate:
fn calculate_something() -> f64 {
let big = get_big_number();
let small = get_small_number();
assert_ge!(big, small); // >= or panic with message
(big - small).sqrt
}
It turns out it's even better if your code has good error handling, such as a runtime assert macro that can return a result that is NaN (not a number) or a "maybe" result that is either Ok or Err.For example with Rust and the Assertables crate:
fn calculate_something() -> Result(f64, String) {
let big = get_big_number()
let small = get_small_number()
assert_ge!(big, small)?; // >= or return Err(message)
(big - small).sqrt
} >>> big = 2; small = 3
>>> assert big >= small, "big is smaller than small"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError: big is smaller than small
In pytest, assertions are rewritten[1] to return something more useful like: def test():
big = 2
small = 3
> assert big > small
E assert 2 > 3
test/test_dumb.py:4: AssertionError
========== short test summary info ==========
FAILED test/test_dumb.py::test - assert 2 > 3
1: https://github.com/pytest-dev/pytest/blob/f373974707f57a0b28...The values of the variables can be included in the generic macro's message via a custom message format, like `assert!(foo >= bar, "foo = {foo}, bar = {bar}");` but having the macro do it by default is convenient. There is an old discussion to have the `assert!()` macro parse its expression to figure out what variables are there and print them out by default, but it's still WIP. ( https://github.com/rust-lang/rfcs/blob/master/text/2011-gene... https://github.com/rust-lang/rust/issues/44838 )
============================= test session starts ==============================
platform linux -- Python 3.11.2, pytest-7.2.1, pluggy-1.0.0+repack
rootdir: /tmp/dl/py
collected 1 item
test_bigsmall.py F [100%]
=================================== FAILURES ===================================
________________________________ test_something ________________________________
def test_something():
> assert get_big_number() > get_small_number()
E assert 0 > 1
E + where 0 = get_big_number()
E + and 1 = get_small_number()
test_bigsmall.py:8: AssertionError
=========================== short test summary info ============================
FAILED test_bigsmall.py::test_something - assert 0 > 1
============================== 1 failed in 0.06s =============================== CL-USER 1 > (defun calculate-something (big small)
(assert (>= big small)
(big small) ; these can be reset
"Big ~a must be bigger than small ~a" big small)
(sqrt (- big small)))
calculate-something
The error handling will now offer me to repair it, if necessary: CL-USER 2 > (calculate-something 4 7)
Error: Big 4 must be bigger than small 7
1 (continue) Retry assertion with new values for BIG, SMALL.
2 (abort) Return to debug level 0.
3 Restart top-level loop.
Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for other options.
CL-USER 3 : 1 > :c 1
The old value of BIG is 4.
Do you want to supply a new value? yes
Enter a form to be evaluated:
17
The old value of SMALL is 7.
Do you want to supply a new value? yes
Enter a form to be evaluated:
5
3.4641016
The last number is the return valueNot just for debugging either. Giving something a name gets you to think about what a good name would be, which gets you thinking about the nature of the thing, which clarifies your thinking about the thing, and leads you to better code.
When I've struggled to figure out what the right name for something is, I sometimes realize it's hard because the thing doesn't really make sense. E.g., I might find I want to name two different things the same, which leads me to understand I was confused about the abstractions I was juggling.
But it's also always nice to have a place to drop a break point or to automatically see relevant values in debuggers and other tools.
But I've started to group variables into two groups: Things users aka my fellow admins are supposed to configure, and intermediate calculation steps.
Things the user has to pass to use the thing should be a question, or they should be something the user kind of has around at the moment. So I now have an input variable called "does_dc_use_dhcp". The user can answer this concrete question, or recognize if the answer is wrong. Or similarly, godot and other frameworks offer a Vector.bounce(normal) and if you poke around, you find a Collision.normal and it's just matching shapes - normal probably goes into normal?
And on the other hand, I kinda try to decompose more complex calculations into intermediate expressions which should be "obviously correct" as much as possible. Like, 'has_several_network_facing_interfaces: "{{ network_facing_interfaces | length > 0 }}"'. Or something like 'can_use_dhcp_dns: "{{ dc_has_dhcp and dhcp_pushes_right_dns_servers }}'.
We also had something like 'network_facing_interfaces: "{{ ansible_interfaces | rejectattr(name='lo') }}"'. This was correct on a lot of systems. Until it ran into a system running docker. But it was easy to debug because a colleague quickly wondered why docker0 or any of the veth-* interfaces were flagged as network-facing, which they aren't?
It does take work to get it to this kind of quality, but it is very nice to get there.
Besides a debugger, isn't one of the first things people do (even undergrads) is start logging out variables that may be suspect in the issue? If you have potentially a problematic computation, put it in a variable and log it out - track it and put metrics against it, if necessary. I'm not entirely sure a full article is worth it here.
People have writing code in a certain way to provide logical "breadcrumbs" for a very long time, and doing it very deliberately. The fact that a tool was created that takes advantage of that isn't an "accident."
Compare to: "Correctly-spelled words as accidental hyperlinks to the dictionary definition."
My point was: what a "potentially problematic" computation is is not always known in advance. A style which is rich in local variables, when combined with a tool that shows actual values for all local variables when unhandled exceptions occur gives you this "for free". I.e. no need to log anything.
Really nice to use if you need logs in the terminal
If it was a for loop I'd know at first glance at the exception what exactly failed...
If your language & IDE does not support functional programming properly with debugger and exception reporting - don't do it.
What’s the easiest possible way to cause stacktraces to also dump local variable information? I feel like this is a feature that should be built into the language…
On my local machine it’s even better because I can run the code, let it break and then jump straight into the debugger where I can move up and down through the stack. I can sit in the context at any point and play with the data. I can call any Python function I want to see how the code behaves.
<cough>Bugsink</cough> :-)
With Python, you can run your program under pdb, which will automatically enter break on exceptions, and you can easily print locals.
That is built into Python!
import sys
def div(x, y):
return x / y
try:
print(div(1, 0))
except ZeroDivisionError as e:
print("Div by zero!")
print("Locals:")
_, _, tb = sys.exc_info()
print(tb.tb_next.tb_frame.f_locals)
This will output: Div by zero!
Locals:
{'x': 1, 'y': 0}
https://stackoverflow.com/a/5328139 has a more thorough implementation that will print you entire stack of local variables. def calculate_something():
big_number = get_big_number()
small_number = get_small_number()
return math.sqrt(big_number - small_number)
vs def calculate_something():
return math.sqrt(get_big_number() - get_small_number())
I'll pick the first one every time. This is a bit of an extreme example, but our languages provide us with the ability to extract and name subexpressions, and we should do that, rather than forcing people to parse expression trees mentally when reading code.Like add a line to a log, but only when an traceback is shown.
See: https://zopeexceptions.readthedocs.io/en/latest/narr.html#tr...
Seems like the zope.exceptions package can be used independent from Zope.
What? For whom? I've been extremely intentionally breaking up longer expressions into separate lines with local variables for a long time.
Writing local variables as "breadcrumbs" to trace what happens is one of the very first things new developers are taught to do, along with a print statement. I'd wager using a "just to break things up" local variable is about as common as using them to avoid recomputing an expression.
... Perhaps the author started out with something in the style of Haskell or Elm, and casual/gratuitous use of named local variables is new from that perspective?
> However, the local variables are a different kind of breadcrumbs. They’re not explicitly set by the developer, but they are there anyway.
While I may not have manually designated each onto a "capture by a third-party addon called Bugsink" whitelist, each one is very explicitly "set" when I'm deciding on their names and assigning values to them.
Admittedly this may not be the best choice of words... but it was a good trade-off of length/clarity at the time for me.
The longer version is: an _ideal_ programming language (from the perspective of debugging, though not all other perspectives) would just allow a full reverse playback through time from the point-of-failure to an arbitrary point in the past. A (small) step towards that is the "Breadcrumb" as introduced by Sentry; a hint at what happened before an error occurred. I argue that, in the coding-style as discussed, and when exposing local variables in stacktraces, local variables actually serve as breadcrumbs, albeit not explicitly set using the breadcrumb-tooling.
> along with a print statement
yeah but the point is that in this combination of coding style and tooling print statements become redundant
> third-party addon called Bugsink
If by third-party you mean "the data flows to a third party" you're mistaken, Bugsink is explicitly made to keep the data with you. If by "third party" you mean "not written by either myself or the creators of my language of choice, you're right.
Right now the code is clear. After you’ve had your morning coffee, and read my PR description. How about at 5:05 when you’re trying to get to your kid’s soccer game and there’s a production issue? After I merge this you’re never going to look at this code again when there isn’t something else going on. A bug or a feature you’re trying to wedge into this code.
header = {"name": "Unknown"}
body = {"items": []}
footer = {"page": 0}
placeholder = {
"header": header
"body": body
"footer": footer
}
return(placeholder)