Hacker News new | ask | show | jobs
by tra3 1283 days ago
I struggle with code like this (also, complicated piped shell statements). How do you debug/reason about this? I know this specific example may be an exercise, but still.
7 comments

Ruby is a surprisingly simple language. Metaprogramming in ruby is a pretty good book that makes it clear what is happening under the hood.

The yield is probably the most complicated part of it, but it is extremely useful for hiding complexity. Once you understand yield, there is very little magic to what ruby is doing.

I prefer python to ruby, but the concept is the same:

https://realpython.com/introduction-to-python-generators/

As for understanding the command line/reasoning about it, they are usually generated iteratively.

  Look at a file to see what you have to work with:
    $ cat $file | head -n 5
  Decide commas aren't useful and remove them
    $ cat $file | sed 's/,//g' | head -n 5
  Decide you want a tab between every 4 characters on any given line
    $ cat $file | sed 's/,//g' | sed 's/\(....\)/\1\t/g' | head -n 5
Each step of the way you see the output, and every additional pipe modifies the last seen output.

Everything else is just being aware of what tools you can use (sed/awk/grep/xargs/etc) and the limits of the data you work with.

GP may have done something like popping the ruby repl, irb, and then:

  File.read('input6.txt').chars
  File.read('input6.txt').chars.each_cons(4).to_a
  File.read('input6.txt').chars.each_cons(4).find_index { _1.uniq == _1 } + 4
each_cons was introduced to me by Copilot. This exact line, in fact. For this exact problem.
First, it's an exercise, you don't get to use much of this functionality day to day, at least if you're a lowly webdev :(.

Second, the solution is optimized for speed of writing (you get more points in AOC the quicker you submit the solution), not for readability. I try to get the chain of calls in my Ruby REPL as soon as the challenge drops.

Third, if you so wish to debug this chain of calls, you can insert a breakpoint and get a REPL anywhere in it with Object#tap:

   foo.bar(3).tap { |chain| binding.irb }.baz { |larodi| larodi + 5 }
And lastly, it becomes second nature to read and write these.
> How do you debug/reason about this?

Debug? You don’t. You have to break your sexy chained-functional-style 1-liner into a “boring” multi-line loop to actually set meaningful breakpoints and work through any problems that may arise. Which is why I dislike this style of code — once something goes wrong, it’s WAY more cumbersome to debug and almost always makes you “unroll” it into its boring, “classic” form. And, of course, once you do that, you’re now debugging something DIFFERENT than what’s shipping in production! And you have to be extra careful to ensure that all of the logic has been kept the same, lest you ship a patch that doesn’t actually fix the bug! This is my prototypical “what programming is NEVER about” example: programming is never about how pretty the code is. Programming is about shipping features, and then being able to diagnose and fix problems with what you shipped.

That is both plainly incorrect (you can drop a breakpoint without breaking the chain, see comment below), and missing the point -- I mean, I appreciate the lecture on what programming is all about, but in this case the programming is all about having fun, and writing a solution as fast as possible ¯\_(ツ)_/¯.
So you have to add code to debug it with #tap? I don’t care if the chain is unbroken: you have to CHANGE THE CODE YOU SHIPPED in order to properly debug it. That’s idiotic. And before you say “oh, this is just an exercise”, I can tell you from personal experience that I encounter this endlessly in prod code with both Python and Kotlin chained functional style garbage. It is often not debuggable without break-up and unrolling.
> you have to CHANGE THE CODE YOU SHIPPED in order to properly debug it

If I'm at the point where I need to debug a production process with breakpoints, I'd rather just find a new job than worry about my coworker's coding style.

> Debug? You don’t.

If by “debug” you mean “step through with a debugger” (at least Ruby’s standard debugger), sure (except at the steps with blocks, which you can easily insert as noops as needed with #tap). But, honestly, I almost never resort to using a debugger in practice, so minor variations in where breakpoints can be set aren’t something I see as a big deal.

> and almost always makes you “unroll” it into its boring, “classic” form.

No, it never makes you unroll it in Ruby.

As you get used to it, it becomes highly readable. The 'tap' method to see what the elements look like at an intermediary step is also really helpful. I usually build these up left to right as well though and won't add on the next processing step unless I'm very certain there period ones are correct
It's important to separate this example from the general concept.

The concept is (generally) called functional composition.

The way one reasons about it, is to mentally split the statement at each function, and think about each step separately. The readability of functional composition comes from the fact there is interrelation exclusively between each adjancent couple of functions - in practice, the reader needs to keep only one result in mind at a time.

Functionally composed statements read like a sequence of statements, rather than a single one. The advantage is that they avoid having to use throwaway temporary variables for each step.

Shell pipes work the same. Even if they're long, assuming that they don't obscure features or use complex intermediate results, they're interpreted the same way - one transformation at a time.

Back to the example. It's not good for a few reasons:

1. it's not properly formatted

2. it uses an uncommon feature (named block variable, `_1`)

3. the sum at the end of the statement breaks the flow.

One would typically write the example like:

    index_found = File
        .read('input6.txt')
        .chars
        .each_cons(4)
        .find_index { |sequence| sequence.uniq == sequence }

    index_found + 4
Writing functional composition in Rust tends to be a bit cleaner, not because of the language, but because of the autoformatting, that indents the statement (but not the sum; that one, I've separated it manually).

Regarding debugging: you can split the statement as convenient, and recompose it once you're done with debugging. Depending on the given statement, one can also put breakpoints inside the blocks.

You read it from left to right and figure out what the result of each step (marked by .) is before you tackle the next part of it.
Code like this should definitely not be written in production code bases and is really just a fun exercise. I think everyone would agree that if this had been written in 2 or 3 lines it'd be much more readable (and therefore maintainable)
Truly, if this was not meant to be pasted in a REPL, it'd be in a class, and probably constitute of several descriptively named methods.