Live data from Hacker News

Avoid Indirection in Code

matthewrocklin.com

141–150 of 220 posts

Re: Avoid Indirection in Code

#141
I agree with the conclusion largely, but I think the reasons listed in the article are not good. Personally I feel the problems of doing too much abstraction are belows.

1. It takes a lot of time.

2. You might end up with bad abstration that in the end hinders you. You then have to work around the abstration you created.

3. More functions create more entries you need to understand in your mind, this can sometimes make code harder to reason about.

Re: Avoid Indirection in Code

#142

While the example in the article isn't great for making the point I actually agree with the main idea. I understand the arguments for 'Uncle Bobifying' code but I think, as the article says, there's a balance to be struck. It's highly likely the next time I see your code (or my code if I'm coming back to it a month or two later) is when I need to fix a problem with it. While it's useful to have it split up into logic…

We should extract small functions when it helps understanding the program. Sometimes the code is clearer when we extract a one line function - non-trivial boolean expressions come to mind - and sometimes we should keep a (reasonably) long method that is better understood as it is. As always we should adapt our approach to the situation and not blindingly apply some preconceived rule. One size does not fit all. But ge…

> We should extract small functions when it helps understanding the program.

This. If I may reiterate your point:

`is_foolike(x)` may not be much better than `x.startswith("foo")` but, say, `is_superuser(x)` is much easier to understand than `x.username.starts_with("su_")`. And note how the mental burden is decreased in this case as well, as a first-time reader would not be wondering why we're checking usernames in the middle of a function way past login.

Re: Avoid Indirection in Code

#143
post #128

Earlier quoted context omitted.

Put another way: exceptions are exceptions to the normal program flow. They should not be used to communicate expected errors. I think of them as analogous to the `if (do() != 0) { goto out; }` paradigm of C.

Unless you're using Python where exceptions occur every time you finish looping. https://docs.python.org/3.7/library/exceptions.html#StopIter...

True. However, it is unusual for the user to deal with StopIteration themselves (therefore somewhat exceptional).

Re: Avoid Indirection in Code

#144

Earlier quoted context omitted.

Congratulations, this pattern is responsible for more security bugs than any other. Security should not be an "if" but must. Presumably down to instruction level or preferably to hardware that does TLB. This thing is a likely TOCTOU error. What happens if I pass the check and enter the function just not having the permissions? The only way this is acceptable is if the code is proven to never do that, preferably to th…

It was an example. It could very well have been checkForSomeNonCriticalThing(x).

It's the principle of checking separated from using which is wrong in almost all cases. Instead you should cause and/or handle errors!

(Also known in Python as duck typing when applied to types.)

Re: Avoid Indirection in Code

#145
This is back to the usual intractable problem - naming things. If you name the function properly and it abstracts the operation of the function precisely then you have added information, not taken it away.

You understand what 'has_kettle_boiled' means.

Inlining functions makes it difficult in a sequence to work out what are the separate steps. Splitting those out into a function - particularly if they take the temporary variables with them - makes it easier to understand what the higher level function does, not harder.

Perhaps we should revisit stepwise refinement as a technique and the need for functions to be loosely coupled and highly cohesive.

Re: Avoid Indirection in Code

#146
post #40

It's not indirection , it's bad abstraction that's the real issue here. Consider the example used in the article: if x.startswith("foo"): do_something_with(x) if is_foolike(x): do_something_with(x) The problem with both of these variations is that the "if-statement" doesn't have any meaning behind it. There's no gain in the indirection presented here. Whereas the following code has meaning: if checkHasPermissions(x):…

When the author used generic "foo" examples, it's hard to tell if there's meaning behind it or not. In a real example, there might very well be meaning behind it, perhaps his "foo" examples in his actual life are more like your "checkHasPermissions" example.

But I think you're right that "abstraction" is the important concept here, which "indirection" is either a synonym for or one part of, depending on what you mean by "indirection".

I think the main challenge of organizing your code ("architecture") is figuring out the right abstractions.

And "too much abstraction" is definitely a thing.

In my experience there aren't any magic rules or guidelines you can just follow to get the right level and kinds of abstraction. There are guidelines and things to think about, and while getting more experience writing (and maintaining!) code is the only way you get better at it, reading and thinking and discussing it helps too.

Some things that have been attempts to guide people to the right levels and kinds of abstraction, like "design patterns", have proven to be no magic bullet either.

In actual experience, I think most of us end up erring on the side of too much/too complicated abstraction ("over-engineering"), so when it doubt, it makes sense to try erring on the side of less abstraction, including duplicating code at times (do not let "DRY" be your only guide).

Which, in the end, is what I think OP is telling us.

Re: Avoid Indirection in Code

#147
post #95
post #40

It's not indirection , it's bad abstraction that's the real issue here. Consider the example used in the article: if x.startswith("foo"): do_something_with(x) if is_foolike(x): do_something_with(x) The problem with both of these variations is that the "if-statement" doesn't have any meaning behind it. There's no gain in the indirection presented here. Whereas the following code has meaning: if checkHasPermissions(x):…

In many cases speaking of the foolike nature of a value is not dissimilar to speaking to the primeness of an integral: Is a value prime? And now here's is an algorithm for determining whether this value is prime. The motivation for many splitting out a prime-testing function is that primality testing is hard to do efficiently, not that there are many kinds of primes or many kinds of integrals. Indeed, in some languag…

> Nothing is being abstracted here either- we're still only talking about giving this algorithm a name

I would say giving something a name that summarizes it so you can refer to it by the name as shorthand (and in code, "referring to" means "using")... is in fact exactly what "abstraction" is, it's possibly almost a good working definition of "abstraction" in fact.

Re: Avoid Indirection in Code

#148

While the example in the article isn't great for making the point I actually agree with the main idea. I understand the arguments for 'Uncle Bobifying' code but I think, as the article says, there's a balance to be struck. It's highly likely the next time I see your code (or my code if I'm coming back to it a month or two later) is when I need to fix a problem with it. While it's useful to have it split up into logic…

"While it's useful to have it split up into logically discrete chunks with sensible naming these 2-4 line functions far increase the mental load of the code."

I don't do this a lot, but in those cases where I'm tempted to pull out "helper functions" that I just call once, simply so I can give them a name, I often use blocks instead. That is, instead of:

    func DoTheThing(...) {
         x = doTheFirstPart(...)
         y = doTheSecondPart(...)
         return doTheThirdPart(x, y)
    }

    func doTheFirstPart(...) { ... }
    func doTheSecondPart(...) { ... }
    func doTheThirdPart(...) { ... }
I'll do:

    func DoTheThing(...) {
        // Do the first part
        var resultType1 x
        {
            // basically the contents of doTheFirstPart here
        }

        // Do the second part
        var resultType2 y
        {
            // basically the contents of doTheSecondPart here
        }

        // Do the third part
        {
            // doTheThirdPart here
            return final result
        }
    }
If you're literally going to call the function only once, and it just disrupts the flow, this has a number of advantages. In most brace-using languages, the braces will confine a scope just like a function would have, so you still have clear specification of what can flow out of the brace functions via the previous variable declarations. (They can stack up a bit if you have a lot of these sections but I've not had too much trouble with that.) If you do need to pull it out into a function later, it's a fairly mechanical process instead of a surgical process, because you're already 90% of the way there. But if you never need to do that, you retain the ability to simply read through a function and see everything it's doing.

I've noticed a lot of my "payload" functions sometimes end up long, but if you look carefully, you'll see I'm still using a lot of tools for scoping and simplification to confine action-at-a-distance within the function itself.

You do get a couple of weird looks and possibly some weird review comments if you use this; people are not used to seeing scopes used solely for isolation like this. I've had people ask me whether this is a syntax error or not in the review. But usually once you explain yourself it passes through.

This seems to mostly end up in my testing code, where I may need a chunk to set up the environment for a particular chunk, and then may have some tests I want mostly isolated from each other, but to use the same environment, or in a recent case, I had a chunk that setup the unique environment for the test, the code under test was basically "testEnvironment.DoIt(...)", and then I had half-a-dozen blocks verifying that the DoIt call had done all the things it was supposed to do and had all the exact side effects it was supposed to have, one class of them at a time, confined in braces so it's syntactically enforced they can't have leaking values. (This was an integration-level test.)

There are definitely other cases where simply naming things and breaking them into functions can be helpful, so you end up with an "orchestration" function that reads cleanly, and all the little bits also read cleanly. I do that plenty too.

Re: Avoid Indirection in Code

#149
Well, indirection can be quite helpful even for simple checks:

  if is_json(somestring) {
      // do something
  }

  function is_json(string s) {
      return JsonLib.convert(s) != NULL
  }
this indirection is easier to read rather than thinking too much about what JsonLib.convert(s) != NULL mean. This does not hinder debugging because you only debug this is_json function when the code does not evaluate is_json as expected. Every other time, you debug what's in the if statement.

The given example x.startswith("foo") is simple enough to include it directly. However, if there is a business reason for why "foo" is checked here, it will be difficult to understand why the check is there. This will be more difficult to read a code like this:

  if x.startwith("-")
      do something
instead of

  if is_yaml_node(x)
      do something

  def is_yaml_node(x)
      x.startwith("-")

Re: Avoid Indirection in Code

#150
post #58
post #56

The point made by the article (avoid using due to problems for debugging and code review etc.) does not hold much water. 'Indirection' can be invaluable while separating 'how you got it' from 'what to do with it' (it=some data), for example. 'what you do with it' can remain unaffected by changing 'how you got it'. This will not mess with code review practices but make understanding the pieces easier. It will also mak…

Yup, seems like the author had trouble following badly written abstraction and/or indirection and came up with the idea that they are bad. Abstractions are invaluable if you are dealing with a large codebase. Even the original author can’t keep everything in their head. Indirections are invaluable if you want to be able to modify code in the future without making a gigantic mess — and potentially breaking compatibili…

Well, good abstractions/indirections are hard.

The wrong abstraction can be worse than no abstraction at all. And it can be non-obvious if you've got the "wrong" abstraction until much later when it's been in use for a while and had to be maintained -- and sometimes even then, we aren't good at recognizing that our pain is coming from the use of the wrong abstractions.

We often are, without necessarily realizing it, reluctant to refactor existing abstractions once there, preferring to alter them slightly or even more add new ones on top (which can make things worse in the long-run). And this isn't necessarily irrational, refactoring/removing existing abstractions is expensive, and with no guarantee you'll get it "right" this time either.

Sandi Metz says:

> prefer duplication over the wrong abstraction

https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstracti...

Mechanically extracting things as in OP can often lead to "wrong" abstractions, which is what I get as the takeaway from the OP, which is a good reminder.

You are right that abstraction is the main tool we use as computer programmers, one way or another. Thinking in abstractions is the main thing we do that characterizes computer programming in contrast to other endeavors. So we certainly can't give it up entirely.

But this fact, I think, from my observations over my career, often leads us to over-abstraction ("over-engineering"), and the wrong abstractions. Because we are so taken with abstraction (it is neat, and you probably think so if you enjoy computer programming). It is good to be reminded that there is such a thing as too much as well as too little abstraction. Sometimes it's the "wrong" abstraction, but sometimes the "right" abstraction and the time it would take to arrive at it were unnecessary, and less/no abstraction would have served you just fine.

Post reply on HN