The ‘puzzle’ of imperative languages is often ‘how do I keep less state?’. Because they're so verbose, figuring out what the code _does_ (in terms of actual transformations to the data) usually involves imperatively running the program in your head, and so it's important to keep the amount of state that could be touched in any particular part of the code to a minimum, since humans only have so much working memory.

When I write code in an imperative language, I often spend a lot of time trying to find the optimal balance between complexity in the data and complexity in the control flow. As a dumb example, if you have a loop over some data that does one thing up to a certain point then another thing for the rest of the data, you can a) keep a Boolean that indicates whether the switch-over condition has been reached, or b) break the loop up into two loops, and fall through to the second loop once you hit the condition in the first loop. Without additional constraints I often prefer the latter (keep as much state in the control-flow as possible), but it can be very puzzling to figure out how to map your problem into a linear state machine, or to try to group lines of code into blocks that share invariants without accidentally overwriting some important data.

C is particularly designed around this kind of puzzling, and has features carefully built in to enable and encourage it, such as the pre- and post-increment operators, or assignment returning its RHS. C code considered elegant has usually been thoroughly optimized in this way. A prime example is this argument-parsing code from K&R [1]. When reading, I find it interesting to bear in mind that this is pedagogical code intended to teach people how to write the language, written by the authors of the language itself. Python tried to take away the ability to optimize for this sort of elegance by removing the syntactic features that support it, but the core puzzle still remains — you can carefully organize your code to ascribe meaning to things like loop breaks (especially with `except`!) or function returns. At its core this is because procedural code gives you a particular active data structure to work with (a stack of function calls each consisting of a set of mutable variables and a list of statements that mutate those variables, possibly with loops in) that is insufficient for encoding the natural structure of many programming problems, and you so have to make choices about which bits of the problem to map to it and which to code up manually with explicit additional data structures. This is not specifically a criticism of procedural languages: all other paradigms also limit you to one data structure or another, FORTH being the one that is most up-front about it.

In general, I think it is a fallacy of perspective to think that the difficulty of programming is broken down into ‘understanding what the code should do’ and ‘explaining that thing in your programming language of choice’. Rather, the problem of programming is precisely ‘understanding what the code should do in the computational model used by your programming language of choice’. If there is a universal model of computation in which we (as human programmers working on commercial timescales) can understand the meaning of programs in a way that is completely independent of the target language (such that mapping it to any target language is a purely mechanical process) I don't think we've found it yet. Someone steeped in a particular programming model will tend to consider their model effectively universal, and translating their mental model (expressed in terms of that programming model) into a different model of computation to be a puzzle; but in fact even encoding the ‘understood’ behaviour into a real-world programming language based on the same model usually involves making non-trivial decisions, which implies that our mental models of computational models are usually low-fidelity and/or may disagree with reality.

[1]: https://news.ycombinator.com/item?id=16249936