exupero's blog

Implicit stack climbing

In the previous post I hypothesized that boolean function parameters frequently indicate two functions masquerading one, and I advocated for splitting the function into two distinct variants and updating call sites to invoke one or the other function rather than passing a boolean flag. I've often considered that hypothesis when refactoring code that feels awkward. The awkwardness, I think, comes from an unwitting attempt to invert the top-down structure of function invocation.

In function-based programming, decisions flow downward from caller to callee via function parameters, and for a callee to know anything about who called it, some sort of stack reflection is usually necessary, if the language and platform make it possible at all. Climbing the stack isn't a technique I've seen in practice, and I've only dabbled in it when debugging. As an extension to the above hypothesis about boolean flags, however, I've begun to check whether logical branches in a function are a subtle form of stack climbing. Is the function trying to figure out where it was called from?

A boolean flag implicitly categorizes everywhere that calls it into two types, and when it uses the boolean in an if, it's deciding what to do based on those two categories. That may seem like a pedantic distinction, but thinking of boolean flags as stack climbing helps identify other forms of stack climbing. For example, checking external state:

(def state (atom {}))
(defn persist []
  (when (@state :spliced-data?)

State checks are common in object-oriented programming, where the external state is the state of the object that has the persist method ("external" here meaning outside to the function). When the persist function is called from different places, and some of those places first set the spliced-data? external state, the check of that state is an implicit attempt to figure out which category of those call sites invoked the function. That's true of more than boolean state:

(defn persist [new-data]
  (when (contains? (@state :written-ids) (new-data :id))

We usually translate this into the thought, "Has the data already been written?", but the active form of the question is, "Did my caller already write the data?" The caller knows the answer but didn't tell the callee, so the callee has to find out indirectly by checking external state. It implicitly climbs the stack.

Stack climbing isn't some terrible problem in code, but I have occasionally found that it indicates sloppy programming. It's usually easier to add a state or flag check to one function than update all its call sites. Often enough, though, that means I (or another programmer) didn't verify that all the call sites behave as expected. Some call sites may have complicating logic. Being able to spot idiom-level anti-patterns frequently helps suss out larger, more architectural issues.