[HN Gopher] Local Variables as Accidental Breadcrumbs for Faster...
       ___________________________________________________________________
        
       Local Variables as Accidental Breadcrumbs for Faster Debugging
        
       Author : thunderbong
       Score  : 32 points
       Date   : 2024-10-06 02:50 UTC (4 days ago)
        
 (HTM) web link (www.bugsink.com)
 (TXT) w3m dump (www.bugsink.com)
        
       | forrestthewoods wrote:
       | I bloody hate Python stacktraces because they usually don't have
       | enough information to fix the bug. The curse of dynamic
       | languages.
       | 
       | 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...
        
         | aidos wrote:
         | I love debugging Python. The stacktraces are great when logged
         | through Sentry so even on production I can normally spot the
         | bug immediately.
         | 
         | 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.
        
           | vanschelven wrote:
           | > The stacktraces are great when logged through Sentry
           | 
           | <cough>Bugsink</cough> :-)
        
         | nomel wrote:
         | I don't think it's related to being a dynamic language. There
         | are many "pretty exception printers" that will dump all the
         | local and global variables, if you want, even up the stack!
        
         | saila wrote:
         | I don't think Python is special in this regard. I have the same
         | issue with .NET/C# stack traces.
         | 
         | With Python, you can run your program under pdb, which will
         | automatically enter break on exceptions, and you can easily
         | print locals.
         | 
         | https://docs.python.org/3/library/pdb.html
        
       | Freedom2 wrote:
       | > accidental
       | 
       | 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.
        
         | djmips wrote:
         | You missed the point of the article.
        
           | Terr_ wrote:
           | What is it that you believe they missed?
           | 
           | 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. "
        
           | vanschelven wrote:
           | > If you have potentially a problematic computation, put it
           | in a variable and log it out
           | 
           | 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.
        
       | jph wrote:
       | > add assertions to your code.
       | 
       | 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         }
        
         | aftbit wrote:
         | Is there a reason you need specialized assertions over
         | something like Python's generic assert?                   >>>
         | 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...
        
           | Arnavion wrote:
           | Rust has a generic assert too, like `assert!(foo >= bar);`. I
           | assume (haven't used the crate myself) the advantage of
           | `assertable::assert_ge!(foo, bar)` is that it prints the
           | values of foo and bar in the assert message. The
           | `assert_eq!(foo, bar)` and `assert_ne!(foo, bar)` macros that
           | *are* provided by Rust libstd also do this. But the generic
           | `assert!()` just sees the boolean result of its expression
           | and only prints that in its message.
           | 
           | 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 )
        
           | o11c wrote:
           | What's more, pytest errs on the side of "just capture more"
           | and In my experience it's quite useful:
           | ============================= 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
           | ===============================
        
       | rafaelbco wrote:
       | In Zope you can create a local variable named __traceback_info__
       | and its value will be inserted in the traceback. It is very
       | useful.
       | 
       | 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.
        
       | Terr_ wrote:
       | > Accidental
       | 
       | 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.
        
         | vanschelven wrote:
         | > accidental
         | 
         | 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.
        
       | lapcat wrote:
       | > Should you really change your coding style just for better
       | debugging? My personal answer is: not _just_ for that, but it's
       | one thing to keep in the back of your mind.
       | 
       | 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...
        
         | isx726552 wrote:
         | Just wanted to say "thank you" for this article. I found it
         | years ago, probably not long after you initially wrote it and
         | have preached it as widely as possible ever since, both as an
         | IC and as an eng manager. It's one of the best such tidbits
         | I've ever come across!
         | 
         | Edited to add: and thanks for keeping it up to date with the
         | new Swift version!
        
         | Terr_ wrote:
         | And where they aren't effectively "free", either your project
         | doesn't need that performance or you're using the wrong
         | language for the job. :p
         | 
         | (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.)
        
         | spullara wrote:
         | "If you have nested method calls on one line of code, you can't
         | easily set a breakpoint in the middle."
         | 
         | You can now do this in Jetbrains products. Pretty awesome, you
         | can even step through them.
        
       | tetha wrote:
       | This is kind of related to a change I recently made to how I
       | structure variables in ansible. Part of that is because doing
       | even mildly interesting transformations in ansible, filters and
       | jinja is just as fun as sorting dirty needles, glass shards and
       | rusty nails by hand, but what are you gonna do.
       | 
       | 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.
        
       | drewg123 wrote:
       | The problem I always have with locals (in kernel code written in
       | C) is that the compiler tends to optimize them away, and gdb
       | can't find them. So I end up having to read the assembly and try
       | to figure out where values in various registers came from.
        
       | rqtwteye wrote:
       | I have done this since a long time. I always thought I am too
       | dumb to read and debug complex code with multiple function calls
       | in one line. I always put intermediate results into variables.
       | Makes debugging so much easier.
        
       | robdar wrote:
       | I suggested this on a thread in /r/cpp a few years ago, and was
       | downvoted heavily, and chewed out for the reason that coding for
       | ease of debugging was apparently akin to baby killing.
        
       | jmull wrote:
       | I'm a fan of this.
       | 
       | Not 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.
        
       | animal_spirits wrote:
       | The Rich package has trace back support that inspects local
       | variables for every stack in the trace:
       | https://rich.readthedocs.io/en/stable/traceback.html
       | 
       | Really nice to use if you need logs in the terminal
        
       | maleldil wrote:
       | Interesting how this seems to go completely against another post
       | I saw here: https://steveklabnik.com/writing/against-names/
        
       ___________________________________________________________________
       (page generated 2024-10-10 23:00 UTC)