skip to content

Under call-by-value, when is an expensive diagnostic argument evaluated if the check that receives it usually ignores it?

level: juniorimportance: must knowfreq 62%

answer

  1. who pays for an ignored argument
  2. the guard inside runs too late
  3. position of use changes nothing
  4. arguments become values before entry
  5. call-by-value evaluates at the call

basics

~10 s

Call-by-value evaluates every argument at the call site, before the check's body starts. The expensive diagnostic is built on every call, including the calls where the check discards it unused.

solid answer

~40 s

Under call-by-value, arguments become values before control enters the callee, so the body never sees an expression. That means the expensive description is built on every call, and the check's own guard runs afterwards, too late to save the work. Where the parameter is used in the body, whether the body returns early, and how rarely the guard passes all make no difference. Any effects in building the description have already happened as well. The two repairs are a guard at the call site, which duplicates the check's rule at every caller, or a non-strict parameter that the callee evaluates only where it uses it, which keeps the rule in one place.

code

pseudocode · 10 lines
pseudocode
function check(enabled, description)
    if enabled
        record(description)
    return

// build_description walks a large structure and formats it
check(false, build_description(big_structure))

// strict: build_description runs, then check is entered,
// then the guard fails and description is dropped unused

go deeper

for a junior

Remember the one-line rule: with call-by-value the argument is a finished value before the body starts, so an expensive argument costs its full price even when it is thrown away.

for a middle

Be able to explain why a guard inside the callee cannot help, and to describe the alternative: a parameter the callee evaluates only where it uses it, which moves both the work and any failure into the body.

for a senior

Show that you look for this in real code: argument expressions that build large intermediates for calls that usually discard them, and the effects hidden inside those expressions that a reviewer assumed were conditional.

for a principal

The trade-off you own is where the rule lives. A call-site guard is local and obvious but duplicated across every caller; a non-strict parameter centralises the decision at the cost of moving evaluation timing and failure sites into the callee.

## The rule strict evaluation follows Under **call-by-value** — the strict, or eager, strategy — every argument expression at a call site is reduced to a value *before* the body of the callee begins running. The body never receives an expression; it receives a finished value bound to the parameter name. That single rule is the whole of it, and everything else here follows from it. Apply it to the setting in the question. A check is handed an expensive diagnostic description — a formatted dump of a large structure, a joined collection, a rendered snapshot — and that description is discarded whenever the check is disabled. Under call-by-value the description is built on **every** call, including the nine calls in ten where the check throws it away. The check's own guard runs later, and by then the work is already done. ## Where the cost lands - **The caller pays, unconditionally.** The evaluation happens at the call site, in the caller's own execution, before control transfers. - **Position inside the body is irrelevant.** Moving the use of the parameter to the last line, or behind a condition that is almost never true, changes nothing: evaluation already happened. - **An early return rescues nothing.** A body that returns on its first line still received fully evaluated arguments. - **Effects are spent too.** If building the description walks a collection, increments something, or allocates a large intermediate, all of it has already occurred by the time the check decides it does not care. - **The optimiser is not a guarantee.** A compiler may drop an unused argument's computation only where it can prove the expression terminates and has no observable effect. For anything interesting — a read from outside the program, a call through an abstract type, a caller-supplied function — it cannot prove that, and the work stays. ## The three treatments side by side | Strategy | When the argument is evaluated | Argument never used | Argument used three times | |---|---|---|---| | Call-by-value | At the call, before the body runs | Evaluated anyway | Evaluated once | | Call-by-name | At each use inside the body | Not evaluated | Evaluated three times | | Call-by-need | At the first use, result reused | Not evaluated | Evaluated once | Read the middle column: that is the column this question lives in. Strictness is precisely what makes an ignored argument cost something, and the two non-strict strategies agree with each other there — they differ only once the parameter is used more than once. ## The fix, and what it moves The repair is not to make the expression cheaper; it is to stop evaluating it at the call. A **non-strict parameter** is one the language evaluates only where the body uses it, so the check's own decision — enabled or not — now decides whether the description is ever built. Two things move with that: 1. **The decision moves inside.** Every call site inherits the callee's rule instead of restating it. 2. **The work moves later.** When the check is enabled, the same cost reappears, now inside the callee at the moment of use, and any failure while building the description surfaces there rather than at the call. The alternative most teams reach for first is a guard at the call site: test the condition, and only then make the call. That works and it is entirely local. Its weakness is duplication — the rule now lives at every call site, and it drifts the moment the rule changes or a new caller forgets it — which is exactly why languages that care about this case let the callee declare the parameter non-strict instead. ## What this question is not about Call-by-value here is a claim about **when an argument expression runs**, not about what is handed over once it has run. Whether the callee receives a copy of a value or a reference to a shared object is a different axis, and the two are routinely confused because they share the word *value*. A language can be strict and pass references to everything; another can be strict and copy everything. Strictness is evaluation timing; copying is representation. Keep the two apart and this material stops being slippery. ## What interviewers listen for The question is checking that you can say *when*, not that you can recite three names. A strong answer states the rule (arguments are values before the body starts), applies it to the ignored argument (built anyway, every call), and names the consequence in both cost and effects. A weak answer says the argument is "skipped because the body does not use it" — which is the behaviour of the strategies call-by-value is being contrasted with, not its own.

  • Under call-by-value, does it matter where in the body the argument is used?
    No. Evaluation finished before the body began, so the position of the use, the number of uses, and whether the body reaches that line at all change nothing about when or whether the expression ran. Position only starts to matter once the parameter is non-strict, because then the body's demand is what triggers evaluation.
  • Why is a guard at the call site not equivalent to a non-strict parameter?
    Both avoid the work, but the guard copies the callee's rule into every caller. When the rule changes, or a new call site is added by someone who does not know about it, the copies drift and the cost comes back. A non-strict parameter keeps the decision in the callee, where it is written once and applies to every caller automatically.

saying these in an interview costs you the question

  • Says the argument is skipped when the body ignores it
  • Thinks call-by-value is about copying, not about evaluation timing
  • Claims a disabled check costs nothing because it returns early
  • Believes moving the expression to the last argument delays it
  • Assumes the compiler always deletes an unused argument's work