skip to content

Procedural Style

Decomposing a program into subroutines over shared data: call stack, parameter passing, module-level state, top-down design. Interviewers ask where it still beats an object model.

on this pageshow

questions

5

A payroll subroutine changes the pay amount it was given, but the caller still sees the old value — which parameter-passing mode is in use?

level: juniorimportance: must knowfreq 72%

answer

  1. who owns the storage the callee writes
  2. a copy versus the caller's location
  3. handle copied, record still shared
  4. copy-restore writes back at return
  5. aliasing pulls the modes apart

basics

~20 s

Call by value: the routine received a copy of the amount, so its assignment updated only the copy in its own activation record. Call by reference, or returning the new value, would make the update visible to the caller.

solid answer

~40 s

The routine was called **by value**, so its parameter slot held a copy of the argument and the assignment wrote that copy, not the caller's variable. Under **call by reference** the slot names the caller's storage instead, and each assignment is visible to the caller straight away. A third mode, **copy-restore**, copies in at entry and writes the copy back into the caller's variable on a normal return, so the caller sees the change only after the call finishes. The case that catches people out sits between these: if the argument is a handle to a shared record, the handle itself is copied, so writing a field of the record sticks while rebinding the parameter to a different record does not.

code

pseudocode · 14 lines
pseudocode
function raise(amount)              // amount: a copy of the argument
    set amount = amount + 100       // writes the copy only

set total = 1000
call raise(total)
print total                         // 1000 - unchanged

function raiseRecord(rec)           // rec: a copy of the handle
    set rec.amount = rec.amount + 100   // writes the shared record
    set rec = newRecord(0)              // rebinds the local copy only

set r = record(amount: 1000)
call raiseRecord(r)
print r.amount                      // 1100 - the field write stuck

go deeper

for a junior

Know the two basic modes and say which one hides a write from the caller. If the routine got a copy, only a returned value comes back; if it got the caller's storage location, the write is visible at once.

for a middle

Explain the handle case out loud: the handle is copied, the record behind it is shared, so a field write sticks and a rebinding does not. That one sentence settles most arguments about this.

for a senior

Show the design consequence. Make the write path part of the signature — one declared output, or a returned record — so a reader of the call site knows what the call can change without opening the routine.

for a principal

Set the convention for the codebase: which aggregates travel by reference, what the read-only promise means when the mode cannot enforce it, and how that is reviewed. The mode is a contract, and contracts that live only in comments decay.

## What a call actually hands over Every call creates an **activation record** — a block of storage holding the routine's parameters, its local variables and the bookkeeping needed to return. The **parameter-passing mode** is the rule that decides what lands in a parameter slot of that record: a *copy of the argument's value*, or a *reference to the storage the caller used*. Every "why didn't my update stick?" question reduces to that one distinction — did the callee write the caller's storage, or its own? Procedural design leans hard on this. A procedural program is subroutines over shared data, so the calling convention is where the contract lives: a signature that says nothing about who may write what leaves the reader guessing, and the guess is wrong about half the time. ## The three modes - **Call by value.** The parameter slot holds a copy of the argument. Assignments inside the routine touch the copy and die with the frame. The only channel back to the caller is the return value. Cost: copying, which matters once the argument is a large aggregate. - **Call by reference.** The parameter slot holds the *location* of the caller's variable. Every assignment is a write to the caller's storage and is visible immediately, including part-way through the routine and including if the routine then fails. - **Copy-restore** (also called value-result). A copy goes in at entry, and on a **normal** return the copy is written back over the caller's variable. It looks like call by reference from the outside until something disturbs that assumption. | Mode | Parameter slot holds | When the caller sees a write | Main hazard | |---|---|---|---| | By value | a copy of the argument | never — only via the return value | copying cost for large aggregates | | By reference | the caller's storage location | immediately, at each assignment | aliasing; unclear write ownership | | Copy-restore | a copy, written back at exit | only on a normal return | diverges from by reference under aliasing | ## Where reference and copy-restore come apart Two situations separate them, and both show up in real batch code: 1. **Aliasing.** Pass the same caller variable as two parameters and let the routine write both. Under call by reference the two parameters are the same storage, so the second write overwrites the first. Under copy-restore there are two independent copies, and the write-back order decides which one survives. 2. **Abnormal exit.** If the routine fails part-way, call by reference has already published every write it performed; copy-restore has published none, because the restore never ran. That is why a language's documentation for these modes reads so fussily, and why languages differ in which modes they offer at all — some provide only one and simulate the others, some let the caller choose per argument, some let the declaration choose. Say that they differ rather than assuming one model. ## The case that actually confuses people Most arguments in modern code are not plain numbers; they are handles to records that live elsewhere. Passing such a handle **by value** copies the handle, not the record. So: - writing a **field** through the parameter changes the one shared record, and the caller sees it; - **rebinding** the parameter to a different record changes only the callee's copy of the handle, and the caller sees nothing. Both happen in one routine and look identical in the source, which is why "objects are passed by reference" is such a durable misconception. The handle was passed by value; the record was shared. ## Choosing a mode in a procedural design - Prefer **by value plus a return value** for small inputs: the signature then tells the whole truth about who changes what. - Where the aggregate is large and genuinely read-only, pass a reference and say so in the name or the comment; the saving is real and the promise is the thing being bought. - Where a routine must update the caller's data, make that its *declared* job — one designated output parameter, or return the updated record — rather than quietly writing through several parameters. - Never rely on an argument being "the same object" as another argument; aliasing is the corner every mode handles differently.

  • Name a program that behaves differently under call by reference and under copy-restore.
    Pass one variable as two parameters and write both inside the routine. Under call by reference the parameters are the same storage, so the second write wins outright. Under copy-restore there are two copies and the write-back order decides. A routine that fails part-way also differs: by reference its partial writes are already visible, while copy-restore publishes nothing because the restore never runs.
  • A routine takes a large employee table it only reads. Which mode, and what does it cost you?
    Pass a reference: copying the table per call is the dominant cost and buys nothing. What you give up is the guarantee the copy provided — nothing in the mode stops the routine, or anything it calls, from writing the table. The read-only promise now lives in a name, a comment or a review rule rather than in the calling convention.

saying these in an interview costs you the question

  • Claims every record argument is automatically passed by reference
  • Treats mutating a field and rebinding the parameter as the same write
  • Says call by value is cheaper because nothing is copied
  • Believes copy-restore and call by reference can never be told apart
  • Thinks the caller's variable name affects what the callee writes
open as a page

For a small nightly batch tool over one record shape, what argues for plain subroutines instead of an object model?

level: seniorimportance: must knowfreq 62%

basics

~20 s

Plain subroutines win where the record shape is fixed and shared, variation points are few, and the whole run reads top to bottom. An object model charges indirection for substitutability a one-shape batch tool never exercises.

open as a page

A payroll routine recursing down a long management chain exhausts the call stack — what does each nested call push onto it?

level: middleimportance: should knowfreq 52%

basics

~20 s

Each nested call pushes an activation record: the return address, the saved caller bookkeeping, the parameter copies and the routine's local variables. Stack use is call depth multiplied by frame size, so a long chain exhausts the region.

open as a page

You decomposed a payroll run top-down into nested subroutines — what signals that the decomposition has stopped paying for itself?

level: middleimportance: should knowfreq 42%

basics

~20 s

Decomposition stops paying when a split only relocates code: routines called once whose names restate their bodies, parameters threaded through layers that never read them, and any new field requiring an edit at every level.

open as a page

Two payroll routines both write a module-level running-total ledger — what does that shared state cost you when a third routine joins?

level: seniorimportance: should knowfreq 58%

basics

~20 s

Module-level state acts as an argument no signature declares. A third writer couples silently to the other two, turns call order into an unwritten contract, and forces every test, retry and concurrent run to reset the ledger first.

open as a page