skip to content

When a construct both denotes a value and performs an effect, what does nesting it inside a larger expression cost?

level: seniorimportance: nice to knowfreq 30%

answer

  1. two properties, not one
  2. denoting a value is not enough
  3. the nested form also acts
  4. position now decides when it fires
  5. an enclosing form may skip it

basics

~20 s

It costs the reader the ability to read the enclosing expression for its value alone. The nested form also acts, so the effect now happens wherever that sub-expression sits and only if the surrounding form evaluates it.

solid answer

~50 s

Denoting a value and performing an effect are **two independent properties**, and expression-orientation only guarantees the first. A construct that has both can legally be dropped into an argument slot, an operand or a condition — and the effect travels with it, into a position where readers do not look for one. Two things get worse at once. Where the effect happens is now decided by where the sub-expression sits rather than by a line of its own, so a reviewer scanning for writes has to read inside every expression. And whether it happens at all can depend on the enclosing form: an operator that does not evaluate both of its sides may skip the sub-expression entirely. The fix is not to abandon expressions but to keep the acting part as its own step, bind its result to a name, and let the expression read the name.

code

pseudocode · 10 lines
pseudocode
// the condition records the attempt AND yields a count
if recordAttempt(student, score) > retryLimit then
    flag(student)
end

// the effect is its own step; the condition only reads a value
constant attempts = recordAttempt(student, score)
if attempts > retryLimit then
    flag(student)
end

go deeper

for a junior

Know that a construct can both yield a value and change something, and that the value being right does not mean the change is obvious to the next reader.

for a middle

Explain why the two properties are independent, and what a nested acting construct does to the reader's assumption that a sub-expression can be read for its value alone.

for a senior

Show the diagnosis: a write buried in a condition, an enclosing form that sometimes skipped it, and how you lifted it to its own step without abandoning the expression style around it.

for a principal

Own the standard — where acting constructs may appear in an expression-oriented codebase — and defend it against the argument that expressions are simply tidier.

## Two properties, often confused for one Two separate questions can be asked of any construct: 1. **Does it denote a value?** If yes, it is an expression and can be nested. 2. **Does it perform an effect?** If yes, running it changes something observable — state, control flow, the outside world. These are independent. A construct can denote a value and do nothing else; it can perform an effect and denote nothing; and it can do **both**. The third combination is where trouble lives, because the first property grants it entry to places the second makes it unwelcome. A candidate who has learned that "expressions compose" sometimes hears this as "if I make everything an expression, my code composes". Expression-orientation removes one obstacle — the form now has a value to pass around — but it says nothing about whether reading that value in place is safe. ## What nesting quietly assumes When you drop a sub-expression into a larger one, you lean on assumptions you rarely say out loud: - that the sub-expression can be understood on its own, from its value; - that moving it — into a name above, into a different operand position — leaves the program's behaviour alone; - that it is evaluated exactly once, when the enclosing form is evaluated. A form that also acts breaks all three. Understanding it now requires knowing what it does as well as what it yields. Moving it moves the effect. And the third assumption is the sharpest: enclosing forms are not obliged to evaluate every part they contain. An operator that decides its result from one side alone may never evaluate the other, and a choice evaluates only the arm it selects — so the effect fires or does not depending on a sibling's value. ## The rubric case ```pseudocode // the condition records the attempt AND yields a count if recordAttempt(student, score) > retryLimit then flag(student) end // the effect is its own step; the condition only reads a value constant attempts = recordAttempt(student, score) if attempts > retryLimit then flag(student) end ``` Both shapes record the attempt and both flag the same students. The difference is what a reviewer has to do to find the write. In the first, the write is inside a test — a position where a reader expects a question to be asked, not an answer to be filed. Put that condition behind an operator that may skip its right side, or into an arm of a larger choice, and whether the attempt is recorded at all becomes a property of code written somewhere else. The second shape says the same thing in two lines and keeps each construct to one job: the call acts and yields, its result is bound, and the condition reads a name. Nothing is hidden. ## The assignment case The sharpest version appears where **assignment itself denotes a value**. In such a design `band = bandFor(score)` is not only a write; the whole thing evaluates to something and can therefore be nested — inside a condition, inside a call argument, inside another assignment. The shape is famous for two failure modes: a comparison mistyped as an assignment reads as a perfectly valid condition that also rewrites the name it was meant to test, and a chain of nested assignments makes the order in which names are updated depend on the evaluation rules of the enclosing operators. Languages differ in whether they allow this at all — some make assignment a statement precisely to close the door, some allow it and warn, some allow it freely — which is itself the tell that it is a design trade-off rather than an oversight. ## How to judge it in review | the construct | safe to nest? | why | |---|---|---| | denotes a value, no effect | yes | reading it in place tells the whole story | | performs an effect, denotes nothing | cannot nest | there is no value to consume | | denotes a value and writes state | with care | the write moves with the sub-expression | | denotes a value and transfers control | rarely | the enclosing form may never finish evaluating | The working rule is short: **one job per position**. Let the acting construct have its own line and its own name; let the expression that reads the name stay readable as a value. This costs a line and buys a reviewer the ability to find every write by scanning the left edge of the code rather than the interior of every condition. ## Why this is a senior question Juniors are taught that expressions are the tidier form, and they are usually right. The experienced version of the judgement is that the tidiness is only real when the nested parts can be read for their values — and that a codebase which nests acting constructs has made itself *harder* to review than the statement-shaped version it replaced, while looking more modern. Being able to say where the line sits, and to fix a case by lifting the effect out rather than by abandoning expressions, is what separates the two answers.

  • How would you rewrite a condition that hides a write?
    Lift the acting call onto its own line above the condition, bind its result to a name that says what it is, and let the condition compare the name. The behaviour is identical when the condition was always evaluated, and the write is now in the one place a reviewer looks for writes.
  • Why does the problem get worse when the enclosing form may skip a part?
    Because whether the effect happens at all stops being a property of the line it is written on. An operator that settles its result from one side alone, or a choice that evaluates only the selected arm, may never reach the nested form — so a change to a sibling sub-expression can silently stop a write from happening.
  • Does this mean expression-oriented code is worse for effectful work?
    No — it means the two properties must be tracked separately. Expression-oriented code stays readable as long as the acting constructs are kept at the outer level, each on its own step, and the nested parts are the value-denoting ones. The style fails only when it is applied as a rule about form rather than about what each construct does.

saying these in an interview costs you the question

  • Says an expression is free of effects by definition.
  • Believes making everything an expression alone makes code composable.
  • Thinks a hidden write is fine because the value is correct.
  • Assumes every nested sub-expression is evaluated exactly once.
  • Claims lifting the effect out is only a formatting preference.