
A deep read of Debasish Ghosh's 4 October 2026 essay, which follows "push ifs up and fors down" into a relational query planner and the algebra of filter and map, and names the precondition each rewrite needs.
filter and map. The rule says to move branch decisions toward the caller and loops toward the data. Ghosh asks the question a reviewer asks at the diff: when is the rewrite legal, and when does it pay?Centralize control flow. When splitting a large function, try to keep all switch/if statements in the "parent" function, and move non-branchy logic fragments to helper functions. Divide responsibility. All control flow should be handled by one function, the rest shouldn't care about control flow at all. In other words, "pushifs up andfors down".— TigerBeetle, TIGER_STYLE.md
// before: the callee decides what to do with a missing walrus
fn frobnicate(walrus: Option<Walrus>) {
let walrus = match walrus {
Some(it) => it,
None => return,
};
// ...
}
// after: every caller has already answered the question
fn frobnicate(walrus: Walrus) {
// ...
}// before
for walrus in walruses {
frobnicate(walrus);
}
// after
frobnicate_batch(walruses);Vec<Walrus> to the batch function. 1 The hot loop then runs with a branch outside it.Option<Walrus> is a sum type: a None, or a Walrus. In set terms it is the coproduct 1 + Walrus, a one-value type beside the walruses. 5if up splits the pair: the caller handles the 1, and the callee receives only the Walrus side.filter p (map f xs) -- p inspects the output of f
map f (filter p xs) -- p inspects the input of fp has type b -> Bool; the second has type a -> Bool. Swapping the order swaps what the predicate sees, so the two expressions describe different programs.filter p . map f == map f . filter (p . f)Maybe. filter can be built from a step that keeps or drops each element and a step that collects the survivors. catMaybes :: [Maybe a] -> [a] does the collecting. 9keep :: (a -> Bool) -> a -> Maybe a
keep p x = if p x then Just x else Nothing
filter p = catMaybes . map (keep p)catMaybes works at every element type at once, which is what makes it a natural transformation: mapping a function over the lists commutes with collecting the Justs. 10 filter p fixes its element type, so the naturality has to live in catMaybes.filter p . map f
== catMaybes . map (keep p) . map f
== catMaybes . map (keep p . f)
== catMaybes . map (fmap f . keep (p . f))
== catMaybes . map (fmap f) . map (keep (p . f))
== map f . catMaybes . map (keep (p . f)) -- naturality of catMaybes
== map f . filter (p . f)
[A] to [B'] agree: map and then filter on the upper route, filter on p . f and then map on the lower. The bottom-left corner is the subset of inputs that satisfy p . f; the bottom-right corner is the subset of f's outputs that satisfy p.f runs, and only when the test itself gets cheaper.filter (p . f) still calls f on every element, because it has to compute the test. 1 The rewrite pays when p . f collapses into something cheap on the input, which usually happens when p reads a field that f passes through untouched.people.map(toDto).filter(dto => dto.age >= 18) becomes people.filter(p => p.age >= 18).map(toDto). The rewrite is legal when toDto carries age over unchanged, and then toDto runs only on adults. If p needs a value that f computes, the composite costs as much as f and the rewrite saves nothing.if out of a loop requires a loop-invariant condition. A condition that varies per element can leave the loop only by moving to the boundary and being recorded in a type, as Walrus replaces Option<Walrus>. 1p . f simplifies.A -> B to [A] -> [B] pays the setup cost once per batch, and a batch of one pays it every time. 1p . f with f. When the test costs as much as the work, the rewrite is legal and pointless."If you think a bit, the push-ifs-up-fors-down principle has far broader applications.""So, it's the algebra that tells you which rewrites are legal. In the filter/map case above, it's the naturality ofcatMaybesthat drives the legality and helps you reason about the overall structure of the code.""For the "fors-down" part, it's more about the cost than the equivalence. You change the shape of the arrow fromA -> Bto[A] -> [B]so that you pay the setup cost once per batch."— Debasish Ghosh, Push Ifs Up and Fors Down: The Idiom, Its Algebra, and Its Limits
debasishg.github.io
tigerbeetle.com
debasishg.github.io
matklad.github.io
en.wikipedia.org
en.wikipedia.org
en.wikipedia.org
duckdb.org
hackage.haskell.org
en.wikipedia.org
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.