Hacker News new | ask | show | jobs
by barrkel 3630 days ago
Define true as a lambda taking two lazy values that returns the first, and false as one that returns the second, and you can turn all booleans into lambdas with no increase in code clarity.

The straw man in the post - talking about a case-sensitive matcher that selectively called one of two different functions based on a boolean - is indeed trivially converted into calling a single function passed as an argument, but it's hard to say that it's an improvement. Now the knowledge of how the comparison is done is inlined at every call point, and if you want to change the mechanism of comparison (perhaps introduce locale sensitive comparison), you need to change a lot more code.

That's one of the downsides of over-abstraction and over-generalization: instead of a tool, a library gives you a box of kit components and you have to assemble the tool yourself. Sure, it might be more flexible, but sometimes you want just the tool, without needing to understand how it's put together. And a good tool for a single purpose is usually surprisingly better than a multi-tool gizmo. If you have a lot of need for different tools that have similar substructure, then compromises make more sense.

This is just another case of the tradeoff between abstraction and concreteness, and as usual, context, taste and the experience of the maintainers (i.e. go with what other people are most likely to be familiar with) matters more than any absolute dictum.

8 comments

Someone else addressed the details of your counter argument, but I'd like to respond to it generally.

It seems like every time someone writes an article on how to write better code, there are responses about how it doesn't make sense when taken to some logical extreme, or some special case, as if that invalidates the argument. (FP techniques in particular seem to provoke this.) But code design is like other design disciplines-- good techniques aren't always absolutes.

Do you really think that because the given example doesn't apply to every situation it's a 'straw man'? It is a little tiring to hear all code design advice dismissed this way.

Reading this article I immediately saw a couple of drawbacks. Since then I've thought of a few more. But several of the points made were not lost on me. This article made me think a bit, and I'm still thinking about it. That's worth something right there.

To anyone out there who clicked through to these comments and is thinking it's not worth reading the article, please go ahead and read it. It's short enough. You may or may not use fewer if-statements in the future, but it might give you a better sense of why you choose to do things one way over another.

The article isn't balanced. It's suggesting that the direction of the refactoring is an unalloyed good, as I read it. I disagree.

I've seen junior devs take this kind of stuff literally and over-apply it, like it's a religious ritual that they get a pious buzz from adhering to. I'd prefer people to think first before regurgitating what they most recently learned.

I agree wholeheartedly. I think the boolean blindness concept that's in the background of this article is incredibly important. But if you're going to propose an actual concrete solution, you need to assess whether it will be right all of the time, most of the time, or situationally (edit: and any of those answers is ok--it's fine to have a pattern that sometimes works if its presented as such). That requires looking at the ways it can go wrong. And this article just didn't do that.
The thing is that the tone of the article seems to suggest taking such an extreme: I mean, an "anti-if" campaign? There's like, only one sentence of concession near the end towards those unconvinced by the argument.
FWIW, I'm pretty unimpressed by the anti-if campaign's website. They've clearly put style over substance. It's a beautiful website, but I spent some time poking through it and I can still only guess at what exactly they're on about. It seems to be something about if-statements being bad, but beyond that it's rather a muddle.

I'm trying to be charitable, though, so let's assume that the core of their idea is something coherent. I'm guessing it's really about something I do think is an important point: How inversion of control is a design pattern that lets you create code that's much easier to manage, because it greatly limits the extent to which certain kinds of decisions need to be federated throughout the codebase.

If that's the case, then real sin (and the article author's) is mistaking if statements for the problem. Conditional branching is not a problem; I think most of us can agree it's an essential operation. The real problem they should be after is poor encapsulation. Where if statements come into it is that, if you've got badly architected code with poor encapsulation, one of the symptoms you'll see is that there will be a proliferation of if-statements that crop up all throughout the code. Every single frobinator will need to stop and check whether the widget it's operating on is a whosit or a whatsit before it can take any sort of action whatsoever. Lord help us if we ever try to introduce wheresits into the system; we'll have to go modify 50 different files so we can replace all those if-statements with switch statements.

It's probably nowhere near as fun to write an article that advocates a high level design methodology as it is to write an article that makes a bold contrarian claim like "If statements bad", though.

You are totally right.

As an example, long if/else chains that check state can mean that you need another object, or another virtual function, or some other niblet of orchestration.

Likewise, I'm not really impressed by the anti-if campaign. At some point, abstractions cause the exact same problem they were designed to solve, and produce code that is difficult to reason about or change.

The author is just examining a common design mistake-- there's no sin there. Many times, it's a mistake to pass in a boolean switch when you could instead pass in the predicate function itself. That's a solid example that supports the author's claim. Maybe you're not convinced, but that doesn't mean the article is completely misguided.
Absolutely agree - if a campaign spouts "Destroy All Ifs" that kind of sets the tone for the discussion...
> There's like, only one sentence of concession near the end towards those unconvinced by the argument.

But that sentence is "I’m just joking about the Anti-IF campaign". Why would extra words help?

The title says "destroy all ifs". The author already did take the idea to the extreme himself.
I can't read the author's mind and can't speak for him but I don't think each word (especially the word "all") is meant to be parsed literally.

Here's another website called "Destroy All Software" using a similar phrase: https://www.destroyallsoftware.com/blog

Gary Bernhardt obviously doesn't advocate removing all software from the face of the Earth. Also notice that it includes blog titles with more bombastic titles:

  "One Base Class to Rule Them All"
  "Burn Your Controllers"
Those are probably not meant to be interpreted literally. There really is no single universal class that can be used for all cases in every circumstance. Instead of parsing it literally, it may be a riff on LOTR "the one ring to rule them all."

Likewise, don't eliminate your controllers because he said it's universal advice. Maybe the title is a riff on Cortez "burn your ships" or some other cultural meme like women's liberation of "burn your bra."

It's true. Personally, I took that to be a bit tongue in cheek. I would compare it to "GOTO Considered Harmful"-- where the author wants you to imagine a world without such a technique in order to expand your abilities. (Even though there are probably edge cases where such usage is justifiable.)
The stance is rather different though - "GOTO Considered Harmful" as a phrase is both inviting a discussion and making a limited statement. "Destroy all ifs" is definitive; the argument is over at the end of the phrase and there will be no negotiation or concessions. I know that this is trivial in this case, but I think it would help discourse in the world generally if we could move away from this kind of position taking and offer our opinions more gently.

As a community we should reward more nuanced and open statements.

Absolutely.

Simply replacing "ifs" with anything else.

I think it's clear how this is simply juvenile hyperbolic invective.

"Ifs considered suboptimal" carries the spirit of Dijkstra and the general argument of the anti if folks.
That would work
It is a little tiring to hear all code design advice dismissed this way.

I notice this form of dismissal in virtually all internet arguments. It's like most people aren't aware of the difference between a strong argument and a sound argument.

I think the problem is that most of these types of articles don't take your advice - the "broken" code that they are improving is absolutely wrong, and there's no room for contextual arguments whatsoever. I mean, the article we're discussing is on the topic of eliminating conditionals wherever possible - that's a hardline stance against something so commonplace in programming it's hard to imagine working without it.
> Define true as a lambda taking two lazy values that returns the first, and false as one that returns the second, and you can turn all booleans into lambdas with no increase in code clarity.

This is trivially true, any datatype can be encoded as a function. The post is not saying that we can pass any type of lambda whatsoever, but that we should pass lambdas that implement the required functionality.

> The straw man in the post - talking about a case-sensitive matcher that selectively called one of two different functions based on a boolean - is indeed trivially converted into calling a single function passed as an argument, but it's hard to say that it's an improvement. Now the knowledge of how the comparison is done is inlined at every call point

If call sites shouldn't choose wich lambda (or boolean) to pass, simply define a new function that always passes the same lambda to the original function, and use it everywhere. (This could also be a good case for partial application.)

> This is trivially true, any datatype can be encoded as a function.

To elaborate: this is called the church encoding of the data type. Particularly interesting for recursive data types.

The most common example is probably 'foldr' (or 'reduce' in Lisp-parlance) for linked lists.

That's one of the downsides of over-abstraction and over-generalization: instead of a tool, a library gives you a box of kit components and you have to assemble the tool yourself.

...and a framework is likely to give you a box of components to build a tool-making factory factory factory...

http://discuss.joelonsoftware.com/?joel.3.219431.12

A church encoded boolean is precisely isomorphic to every language's standard booleans (modulo strictness, perhaps) and doesn't offer any benefits; you're still forking the program based on the information content of a single bit.

Let's take the following function invocation, which can be expressed with Boolean literals or Church encoded booleans, I don't care:

  match true false
If you want to determine the significance of the boolean values passed to this function, it does not suffice to go to the definition of 'true' or the definition of 'false'.

Now take something like this:

  match caseInsensitive contains
Even though I have used descriptive names here, it's almost beside the point; I could just have easily have used nonsense names:

  match foobar quux
If you want to know what 'foobar' means, you can go to its definition, and see how it preprocesses a string and a pattern. You don't have to guess about the meaning of a bit.

As a result, the semantics of 'match' and its parameters are all communicated more clearly, with less room for error, and much more generality.

There need not be any syntactic overhead: it is merely the replacement of some flag with a lambda which cleanly encapsulates the effect that would otherwise be encoded in the flag. The way you invoke the function is the same, but instead of twiddling bits to get what you want, you pass functions whose meaning does not require (as much) subjective and possibly error-prone interpretation.

Note this also objectively simplifies the functions themselves, because they formerly contained conditional logic, but once you rip that out and give them no choice (invert the control!), they have less room to err, which makes them easier to get right, easier to maintain, and easier to test.

There is also another way to view the issue: with booleans, we first encode our intentions into a data structure (at the caller site), and then we decode the data structure into intentions (at the callee site).

Well, why are we packing and unpacking our intentions into data structures? Why not just pass them through?

Indeed, we do that by pulling out the code and propagating it to the caller site (possibly with names so you don't need significantly different syntax and can benefit from reuse). Then our code more directly reflects our intentions, because we're not serializing them into and out of bits.

I think the general principle applies to more than booleans, but it's easiest to see with booleans.

Inversion of control both increases the user's power (anything that implements a certain interface can be used) and adds an extra burden. Especially here,

  match caseInsensitive contains
it takes a bit of thought to match the regex-like concept of "Case insensitive match flag" to "case insensitivity can be achieved by a transformation of the pattern and target so that case doesn't matter". Perhaps the right way to relieve this burden is to provide some simple functions that can be used for the common cases (caseInsensitive, caseSensitive) and a sensible default (caseSensitive).
since I'm not a fp expert what about a function like

    ctx.arc(10, 20, 30, 0, 6.28, false);
There's nothing special about boolean. How do you encode all of those above into types in fp so that it's impossible to get them wrong and so they're self documenting? I hope you're not suggesting there be a horizontalFloat type and a verticalFloat type or are you?
How about keyword parameters...

    ctx.arc(center=Point(10,20), radius=30, beginAngle=0, endAngle=6.28, Clockwise);
...and don't forget about units of measurement/dimensional analysis.

https://stackoverflow.com/questions/107243/are-units-of-meas...

    ctx.arc(center=Point(10cm,20cm), radius=30mm, beginAngle=0rad, endAngle=6.28rad, Clockwise);
That helps quite a lot, although for several, it's more programming-by-name than programming-by-semantics.

I'd like to be able to say the end angle has to be less than the start angle, that the unit has to be radians (AKA unit-less :), the unit of radius & that negative values are sensical, and so forth; and have all these properties checked by a compiler.

Which I can do in some modern languages, surprisingly. :)

Assuming you're right about the guesses for those parameters, we could go a little further. Let's define

   data Directionality = Clockwise | Anticlockwise

   data AngularInterval = {
     beginAngle :: Double,
     endAngle :: Double,
     directionality :: Directionality
   }
... and then we're down to three parameters, all of different types (so no opportunity for mistakes, assuming static checking) and who knows, you might even have other uses for AngularInterval.
I'm a big fan of the philosophy that "Every literal in a program is a bug." :)

But I know what you're getting at! Personally, I'm a fan of programming with units and dimensions, and safely representing the distinction between absolute quantities and relative quantities.

That doesn't mean I'd want an infinite number of "float" values for all possible units and dimensions, however; just a powerful enough type system I can give myself some help at compile-time for properly threading sensical values through my programs.

> "Every literal in a program is a bug."

But we use plenty of literals in our programs all the time. Eg lambdas are function literals. (And definitions of named functions are just a special case folding binding and a lambda.)

A library can very easily provide, along with the kit components, convenience functions that perform common tasks - like matchCaseInsensitive or whatever. The point I took from the post is that, regardless of how the final public API is presented (and indeed, hopefully it doesn't involve piecing together umpteen bits), the code implementing it can be written by composing simple components rather than unwieldy conditionals.
#destroyallifs

#notallifs

#carefulwiththoseifseugene
#allifsmatter
#unlessallelse
Will you marry me?
>> you want to change the mechanism of comparison (perhaps introduce locale sensitive comparison)

I couldn't agree more, and this is why I think most FP programs are about as intellectual stimulating as `std::min_element`

How does using if statements make it easier to introduce locale sensitive computation? A locale should be represented in the arguments or as a transformation, very similar to what the article is doing.