MIDREAL

Push ifs up and fors down: The idiom, its algebra, and its limits

Comments

socializer 25h ago on HN
I am continually impressed by the ability of LLMs to take trivial ideas and turn them into lengthy and obtuse blog posts with unnecessary analogies.
bioneuralnet 25h ago on HN
Yet another encroachment on traditionally human activity.
moritzwarhier 25h ago on HN
I am the

  Option<Walrus>
msdz 23h ago on HN
I know we’re not supposed to comment just for that, but this might be my single favorite joke comment I’ve ever read here. Good job.
cowlevel 21h ago on HN
what is the joke
dmoy 20h ago on HN
msdz 4h ago on HN
moritzwarhier 3h ago on HN
That picture is so blurry. Perfect demonstration of everything that's wrong with AI and the internet. Probably uploaded by Claude.
rezonant 21h ago on HN
goo-goo g'joob
jibalt 23h ago on HN
Except that TFA is a bog standard example of traditional human activity and the GP's comment is nonsensical trolling.
swiftcoder 25h ago on HN
Honestly, this just looks like one of those lingo-heavy-but-surface-level blog posts that used to make functional programming spaces so insufferable to everyone on the outside
mahboi 24h ago on HN
These things are so divorced from the reality of programming, even when they involve actual code instead of fancy lingo. Like in Scala, not a pure functional language, tutorials used to find the most convoluted higher-order functional way to do simple things.
ahartmetz 22h ago on HN
You want to avoid branches in hot paths. If you branch inside the loop, lots of branches. If you branch outside the loop (into different specialized loops), few branches. Big fucking deal.
gpderetta 22h ago on HN
econ 22h ago on HN

    bool w;
    int x[1000];
    int y[1000];
    for(int i=0;i<1000;i++){
      x[i] += y[i];
    }
    if(w){
      for(int i=0;i<1000;i++){
        y[i] = 0;
      }
    }
Slower but prettier with the if pushed down.
bzbz 4h ago on HN
Not particularly.

Easier for humans to reason about the branch, when they know that it happens without input from i or y[i].

This is helpful when the `if` statement is complex, or there are multiple if statements, some of which do take y[i] as input.

theteapot 20h ago on HN
Is that true? The loop is a branch.
bathtub365 20h ago on HN
Who actually considers a loop a branch? A loop is a chunk of code that will be run N times depending on some evaluation that’s run before or after an iteration. A branch is a single decision which of several pieces of code to run, once.
ngaiorn 19h ago on HN
>Who actually considers a loop a branch?

The machine?

theteapot 19h ago on HN
> Who actually considers a loop a branch?

A loop contains a branch - keep looping or break. At least according to structured programming -- https://en.wikipedia.org/wiki/Structured_program_theorem.

pasquinelli 18h ago on HN
> Who actually considers a loop a branch?

interesting question...

> A loop is a chunk of code that will be run N times depending

oh! turns out you do!

ismailmaj 10h ago on HN
branchless for loop for funsies

  for (fn, n) = {
    stop = void (i) {}
    run  = void (i) { fn(i); go(i + 1) }
    go   = void (i) { *((i < n) * &run + (i >= n) * &stop)(i) }
    go(0)
  }
Or you could write all the implementations for each possible n and dispatch through a registry.
Jtsummers 17h ago on HN
https://godbolt.org/z/rz8MxhdGd

Just a simple example, but the conditional branch occurs on line 14 of the generated assembly. It does a comparison (line 13) and then a conditional jump (jl, line 14).

swiftcoder 14h ago on HN
> Who actually considers a loop a branch?

Anyone who started out programming in assembly languages

folago 13h ago on HN
> Who actually considers a loop a branch?

The CPU.

mahboi 2h ago on HN
Yeah I was like what, is branching a matter of opinion??
ahartmetz 19h ago on HN
The "go around" branch is obviously not completely avoidable (though unrolling helps). Branches inside the loop body sometimes are.
tremon 8h ago on HN
Not categorically though: an infinite loop does not contain a branch.
mkesper 6h ago on HN
It's just an unconditional branch but still a branch. Probably even not an unrollable one due to non-infinite memory.
mahboi 3h ago on HN
Usually yes, but if inside loop is even more branches. Also obviously wasteful when it's checking a computed condition.
mahboi 3h ago on HN
Even before I understood anything about the machine, I never thought to recheck an unchanging condition inside a loop.
robofanatic 22h ago on HN
And also generate a shorter version.
mathisfun123 22h ago on HN
Semantic compressor and decompressor
adamgordonbell 21h ago on HN
Debasish has been writing for a long time. I have one of his books on FP in my office.
cryptonector 17h ago on HN
No, TFA is standard fare for bloggers who dwell in category theory 24/7.
globular-toast 15h ago on HN
To be fair, I bought Martin Fowler's Refactoring book expecting to learn a bunch of new stuff and up my game. What I found was a bunch of stuff I knew already just from experience but thought was too obvious to enumerate and write down. This was all written a long time before LLMs, of course.

I think the main reason I don't write more is I think once I've thought through something it's too obvious to write down. I consider it a defect of mine and sometimes have to force myself to write.

Gravityloss 11h ago on HN
Maybe dialogue would be a more fruitful approach. Then one would know what the other already knows and the participants could only communicate the "deltas".
tomsmeding 11h ago on HN
Dialogue is super effective for this, but it's also a skill: you're suddenly a teacher. You should have no judgement of the other not knowing something, and be able to effectively build up that delta in a way that is understandable and interesting to the other. This can be more difficult than it sounds, but it's also rewarding.
eyelidlessness 6h ago on HN
It might help to write while you’re mid-trajectory. Post-“obvious” you will then have some more objective material to rediscover what pre-“obvious” you didn’t know yet, and hopefully the insights that helped it feel obvious along the way. Those insights might be the thing another person might find valuable.
mcswell 3h ago on HN
Bizarrely, there is a conversation on exactly this idea of unwritten ideas going on here: https://news.ycombinator.com/item?id=50002650

Here are the particular comments having to do with this (I don't know how to link to an individual post in the discussion...maybe somebody can write that down for me :):

A: There is a vast amount of mathematical knowledge that hasn’t even been written down, much less formalized.

B: There is no such thing as knowledge that has never even been written down a single time by anyone or anything. Those are simply called ideas...

wallstop 24h ago on HN
What is missing here is any benchmarks backing up this argument for code structure.

Of note, as of C#9 (and maybe prior), the dotnet runtime does this automatically whenever it is deemed safe. https://devblogs.microsoft.com/dotnet/performance-improvemen...

The same technique is applied as an optimization, when deemed safe, in all current gen c compilers (gcc, llvm, etc).

I'm very confused why neither measurements nor references to when this is done automatically in most modern languages is included in the article.

cogman10 24h ago on HN
At least in JVM land, it's pretty easy to thwart that optimization. Particularly if the condition is on a mutable yet unchanged in the loop value.

For example:

    var map = new HashMap<String, String>();
    map.put("foo", "bar");
    for (var i : items) {
      if ("bar".equals(map.get("foo")) {
        doStuff(i);
      }
    }
Even though `map` isn't mutated, it's hard enough for the JVM to detect and the underlying `get` functions are complex enough that it'll run the `get("foo")` every time, which can be quite expensive.
Maxatar 23h ago on HN
Can't speak for C# but in C/C++ the optimization can rarely be applied safely due to aliasing. If any part of the data you're working with involves a char* then C/C++ optimizers refrain from doing these kinds of optimizations because of how difficult it is to guarantee the absence of mutability.
HumblyTossed 8h ago on HN
I would rather the developer do it, not the compiler. I don't, as a matter of course, regularly _read_ compiler output. I do, however, read developer output.
ninalanyon 24h ago on HN
I've done this for years. Not every time of course but where it makes the code easier to understand and maintain.

Speed was almost never the reason.

alterom 24h ago on HN
I take it you never rewrote a Matlab for loop as a vector/matrix op for insane speedups then :)
ninalanyon 15h ago on HN
I've never used Matlab, so no.
aappleby 24h ago on HN
I have always phrased this as "Never do one of something".
narnarpapadaddy 22h ago on HN
“Batch is the primitive”
alterom 24h ago on HN
TL;DR in one sentence:

"the loop runs without a branch, and is a candidate for vectorization".

That's it, that's the article. This matters a lot in huge-scale / scientific computing / HPF, where if you can express something as an operation on vectors on matrices, you win big (those ops parallelize well, can be run on GPUs, clusters, what have you).

mypalmike 21h ago on HN
For the 99% of developers who are shuffling data around constrained by I/O, you win small.
OutOfHere 24h ago on HN
I like it, but to do fizzbuzz in this way, you'd have to separate what's inside the loop into a reused function.
pdpi 24h ago on HN
I think it's sort of obvious that the limit to this general rule is when data dependencies between fors and ifs forbid you from pushing things further up/down.
taolson 23h ago on HN
Or just use lazy list operations with a single if test at the end:

https://github.com/taolson/Admiran/blob/main/examples/fizzBu...

/s

dieselgate 24h ago on HN
Didn’t see it mentioned in the article but isn’t leading with if-statement called a “guard clause”. I like that pattern but it’s just general best practice I thought.
woadwarrior01 23h ago on HN
Swift explicitly has a guard statement for this. Rust's let .. else { ... } is also very similar.

https://docs.swift.org/latest/documentation/the-swift-progra...

mahboi 23h ago on HN
Guard clauses are things that return early for trivial or problematic cases. Like https://en.wikipedia.org/wiki/Guard_(computer_science)#Flatt...

They're one of those good practices that look like bad practice to everyone who just got a CS degree. Seems ex-students are unsettled by asymmetry or want to minimize the number of return statements.

ramesh31 23h ago on HN
Particularly for high performance code when branch prediction is taken to account
econ 22h ago on HN
Why am I even in this function if it shouldn't happen?
mahboi 21h ago on HN
If the criteria for it not happening is too complicated to expose to the caller
cowlevel 21h ago on HN
Related: don't write if(arg == null) throw ArgumentNullError; at the top of every function. If it's not supposed to happen then just let it throw the error naturally when you dereference it. In C it's even worse because you turned an easily caught segfault into a silent nop.
preg_match 19h ago on HN
Because a lot of languages can't easily express the semantics of "you're not allowed to call the function this way". I mean, suppose you have a function that takes two integers, but the second must be greater than the first. Most type systems don't have any way to represent that at all, let alone conveniently.
mahboi 21h ago on HN
Oh, guard clauses might also use continue statements in loops
throwawayffffas 23h ago on HN
Just the branch predictor gains are probably worth it.
gorgoiler 23h ago on HN
Erm, no? You write f(w: Walrus) -> Walrus and then let the caller handle Walrus|None and Iterable[Walrus] however they wish!

And if someone decides the codebase needs an abstraction over (and therefore specific functions to handle) Iterable[Walrus|None] then you check the weather and suggest they take a break and go for a stroll. (You check the weather to see if you should lend them your brolly.)

What am I missing?

hatthew 22h ago on HN
Are we talking about this from the perspective of CS (algorithm optimization) or SE (code design)?

From an SE perspective, make a flatmap function that explicitly handles Collection<Optional<Walrus>>. The implementation doesn't matter. If your language/framework already has a compatible flatmap function, make a single frobnicate(Optional<Walrus>) function that returns whatever value is necessary for flatmap(frobnicate) to discard them.

From a CS perspective, doing a filter from Collection<Optional<Walrus>> to Collection<Walrus> is probably a bad idea. If your collection is small, nothing matters. If your collection is large, you probably don't want to spend time making a new copy of it. If your filter just returns a view rather than a hard copy, then there is no optimization benefit and you should just do whatever makes the most sense from an SE perspective. If frobnicate is cheap then you're paying the branch prediction failure tax anyway regardless of when you frobnicate, and if frobnicate is more expensive then your should probably parallelize and have each thread handle unpacking the Optional. Either way, you probably don't want to spend time making a copy.

These are all generalizations based on hypotheticals and there are certainly a lot of exceptions, but broadly speaking I don't see a strong argument here. If optimization matters then optimize based on your own profiling of your situation, and if optimization doesn't matter then design your functions based on what features and paradigms are available/common in your area.

sophacles 5h ago on HN
First: of course it's generalizations. "How to write a program" advice is never about hard rules and is ususally some generalizations and heuristics that when applied well can result in nice programs. When over-applied or treated as hard rules you end up with FacadeFacoryFactories and other such absurdities.

Second: From an SE perspective it has some nice properties too. Obviously every "if" can't be moved up - even the for's that are being pushed down have an implicit "if". But when you try to follow the advice without making the code too crazy, you end up with business logic clustered in a much smaller number of places, and don't have to dig down into a leaf function in an unrelated module to find out why the some transaction was being rejected (aka the accountants said that qty > 100 was not allowed or whatever).

ivanjermakov 22h ago on HN
Save some time and read the original post instead: https://matklad.github.io/2023/11/15/push-ifs-up-and-fors-do...
rtpg 22h ago on HN
I've always believed the opposite: get conditionals deep in your code so that the higher level control flow is regular.

But I suppose my greater philosophy for making code that avoids bugs is that you have a couple things that are done when dealing with data:

- distribution

- deciding

And you want to avoid distribution and deciding being mixed together in the same spot.

"Distribution" can be for loops but also breaking up some data based on some key into N bistinct buckets

"Deciding" is where you're looking at the data more closely to make some decision (like "is this a big customer or a small customer")

Distribution often involves decision making, but if you mix them all in one spot you can obfuscate your decision points. Splitting it up just makes things "obviously" right or "obviously" wrong. Perf stuff is another discussion of course, but in practice most things are not at a scale where it matters.

    by_category = defaultdict(list)
    for d in data:
      by_category[category(d)].append(d)

    for category, per_category_data in by_category.items():
       do_thing(category, per_category_data)
I really value code patterns that make mistakes obvious, or at least makes it harder to stuff a mistake in somewhere. Some patterns are harder to describe in this model though.

(I do like the advice of having a consistent vocabulary for working on collections as a principle though, I just find that top-level conditional use tends to quickly get you into "... why is this method not called" territory, which is a more annoying problem than "why is this slow")

sigbottle 21h ago on HN
That's another good way to look at it - sometimes the "base" is more like a physics substrate. Physics doesn't care about semantics, it just is. Putting semantics first would be weird.

I guess it's a case of perspective

a1o 21h ago on HN
I think compilers can push out ifs inside for to be one if with two fors.
whilenot-dev 17h ago on HN
If the data don't need to be processed in batches by category, and if the category is derivable from an data item alone, I don't really see the benefit you're proposing.

Even worse, by splitting one state (and one derivable category from that state) into two separate arguments for do_thing, something can be off rather badly. I'd then feel the need to design an assertion of the relation of the arguments in order to make things bearable again:

  def do_thing(category, data):
    # to avoid shadowing lets rename your function `category` to `category_from_item`
    assert all(category == category_from_item(item) for item in data)
    ...
But that would add a third loop to your two loops, and would duplicate the computation of a category.

Instead, if category would be a property of data item:

  class DataItem:
    
    @property
    def category(self):
       ...
...and the do_thing function would work on a single data item, then it'd just become a simple matter of one for loop and one match/case:

  def do_thing(item):
    match item.category:
      case ...:
        ...
eska 7h ago on HN
Your category() function contains the branch. So you pushed the condition up, and later loop through each bucket with its corresponding function. So you pushed the loops down. This is typical data oriented programming.
4b11b4 22h ago on HN
this is just a guard on the function definition?
sigbottle 21h ago on HN
Is the idea that "accidental casework" should be moved up, whereas the "reusable bulk ontology" should be moved down?

There's very high-leverage abstractions that completely constrain a space. An example is a good definition - you can't think of something outside to compare it to, it just is. These things survive for a long time since they define it.

But if you're trying to do that philosophy super deep into a program, you're probably violating a bunch of invariants subtly.

Of course, there is no good separation at the end of the day as we all know from spaghetti codebases :)

andy_ppp 16h ago on HN
Idiomatic Elixir does this with pattern matching on function parameters so you end up with things like the following, raw if statements are discouraged because of this:

  def classify(:ok)
  def classify({:error, reason})
  def classify([first | rest])
  def classify(%{name: name, age: age}) when age >= 18
  def classify(%{name: name, age: age}) when age < 18
Chinjut 16h ago on HN
Push lists of 0 or 1 up, but push lists of 0, 1, 2, 3, or etc, down?
Chinjut 3h ago on HN
That is, an optional is just a list of length 0 or 1. But this seems to propose treating that completely oppositely from how it proposes treating other lists.
peterfirefly 15h ago on HN
A better formulation I read in a magazine back in the 90s is: "push mechanism down and policy up".
Toutouxc 11h ago on HN
One thing I noticed is that the principle (better demonstrated in the linked matklad article) seems to make sense at the "a bunch of related functions" level, but it also goes against one of the main principles of OOP, which is not to prescribe behaviors to others.

You don't want to go "woof if you're a dog, meow if you're a cat", you want to go "make a sound, whoever you are". If you're a fish, you can no-op or not "accept" the call, depending on the language and convention. Obviously there is a lot of nuance, but the general idea is to push responsibilities and logic (which includes branching) down, to the level/entity/object that's best equipped to handle it.

If you apply this to the "a bunch of functions" example, you get low-level functions with branching, but you also get clean callers.

I feel like this isn't such a huge problem IRL, it's usually quite apparent where a decision should be made, but both of these "schools of thought" seem equally valid to me.

Sharlin 10h ago on HN
But OOP’s insistence on dispatching "at the leaves" is mostly understood as misguided or overly dogmatic these days. In particular, tagged unions and pattern matching are incredibly useful tools that go right against OO dogma. And they’ve always been so, since their introduction in the early 70s. The entire idea of "objects combine behavior and data" is problematic in many ways, not least because it can be very suboptimal on modern hardware compared to data-oriented approaches.
cypherpunk666 10h ago on HN
roblh 6h ago on HN
I strongly feel that pattern matching is the most useful language feature that there is for writing code that's correct and easy to understand. Once you've used a language that has it, you never really want to go without it again.
SAI_Peregrinus 7h ago on HN
Related is the "parse, don't validate" concept. Pass functions data that is guaranteed to be valid, and they don't have to re-check the same conditions you already checked. The more such re-checks there are, the more work you save. It's also impossible to forget to check the condition in a subsequent function, so you are less likely to have bugs due to invalid inputs.
thefringthing 2h ago on HN
This reminds me slightly of Milne's interpolation theorem for classical propositional logic, which says that any proof can be re-written so that the first use of the law of the excluded middle occurs after the last use of the law of non-contradiction.

Comments are loaded live from Hacker News and are not stored by Mid or Real.