skip to content

A chat client queues one zero-argument function per unsent message — what does holding work as function values cost the queue?

level: seniorimportance: should knowfreq 40%

answer

  1. the queue can only run it
  2. one operation: call
  3. no identity, no key, no dump
  4. code plus captures is not data
  5. a record plus one interpreter

basics

~10 s

Opacity. A queued function value can only be run, never read, so the queue cannot deduplicate, prioritise, report on, persist or retry-with-different-arguments work it cannot inspect. Uniformity is what it buys in exchange.

solid answer

~50 s

Representing each unsent message as a function value makes the queue beautifully general: it takes anything callable, needs no knowledge of what the work does, and never changes when a new kind of work appears. The price is that a function value is a black box with exactly one operation — call it. The queue cannot tell two queued sends of the same message apart from two different ones, cannot order by kind or importance, cannot produce a diagnostic dump more useful than a count, cannot write itself down and reload after a restart, and cannot re-run an item with a changed argument, because whatever it closed over is already baked in. The alternative is to queue a **record** describing the work — kind, payload, attempt count — and keep one interpreter that turns a record into the call.

code

pseudocode · 11 lines
pseudocode
// opaque: the queue's only option is to call it
pending.add(function() send("hi", onAck) end)

// described: readable, sortable, dedupable, storable
pending.add({ kind: "send", text: "hi", attempts: 0 })

function run(item)
    if item.kind == "send"
        send(item.text, onAck)
    end
end

go deeper

for a junior

Know that a queued function value is something you can only call. If you need to know what is in the queue, the entry has to carry data you can read.

for a middle

Explain both sides: why the callable form keeps the queue indifferent to new kinds of work, and which operations — dedup, ordering, reporting, retry — need readable fields.

for a senior

Make the call from the operations the system will need, and say why persistence and operator visibility are the requirements that decide it rather than being bolted on later.

for a principal

Treat it as a durability and operability decision. What survives a restart and what an on-call engineer can see are properties of the representation, and changing it afterwards is a migration, not a refactor.

## Deferred work as a value A function value is behaviour you can hold. That makes it the obvious representation for work you want to do later: build the call now, store it, run it when the connection returns. A chat client's outbox is the canonical case — one zero-argument function per unsent message, drained in order when the socket comes back. The design is genuinely good on one axis. The queue's code mentions no message type, no payload shape and no protocol. Adding a new kind of deferred work — a read receipt, a typing notification, a profile update — requires no change to the queue at all, because each is just another callable. That is the whole payoff of treating behaviour as a value: the decision of *when* is separated from the knowledge of *what*. ## The one operation you get Everything a queue eventually wants to do beyond running items runs into the same wall. A function value supports calling. It does not support reading. - **Deduplication.** Two queued values that perform the same send are not equal in any useful sense, and comparing them does not reveal that they do the same thing. The queue cannot collapse them. - **Prioritisation and reordering.** Sorting requires a key, and there is nothing to key on: every item looks identical from outside. - **Diagnostics.** "47 pending items" is the best report available. Which conversation, which message, how many attempts — none of it is reachable. - **Persistence.** Restart and the queue is gone. A function value is not data you can write down and read back: it is code plus whatever it captured, and reconstructing that in a fresh process is not something holding the value gives you. - **Retry with a change.** A zero-argument value has its arguments baked in. Retrying against a different endpoint, a smaller batch or a new token means building a different value — which the queue cannot do, because it cannot see what the old one was. - **Relevance.** If the user deletes the conversation, the queue cannot find the items that belonged to it. - **Reachability.** Each queued value keeps whatever it captured alive for as long as it sits in the queue, and the queue cannot report what that is. ## Work as a record instead The alternative inverts the trade. Queue a small **data record** — a kind, a payload, an attempt count, a created-at stamp — and keep one interpreter that maps a record to the call it stands for. Now every operation above is available, because the item is readable. Sorting has keys, dedup has an identity, the dump is meaningful, the queue serialises, and a retry can edit fields before re-running. What you give up is the open set. The interpreter must know every kind, so adding a kind means editing it, and the queue is no longer indifferent to what it carries. | | Queue of function values | Queue of records + interpreter | |---|---|---| | Adding a new kind of work | nothing to change | edit the interpreter | | Inspecting an item | impossible | read its fields | | Deduplicating | impossible | compare identity fields | | Surviving a restart | no | yes, if the fields serialise | | Retry with changed arguments | rebuild from outside | edit fields and re-run | | Queue code's coupling to the work | none | knows the kinds | ## Choosing between them The honest rule is to follow the operations the queue actually needs. 1. **If the only operation is "run it, in order, soon"**, function values win outright. The generality is free and the opacity never bites. 2. **If anything must survive a process restart**, the item has to be data. This is usually what decides it in practice, and it is not recoverable later by adding a wrapper. 3. **If operators will ever ask what is stuck**, the item needs readable fields, because a count is not an answer to that question. There is also a middle position worth knowing: keep the function value for execution but pair it with a small descriptor — an identity, a kind, a retry count — in the same entry. Dedup, sorting and reporting then work on the descriptor while execution stays open-ended. It does not buy persistence, since the executable half is still not data, but it buys everything else cheaply. ## Why this is the interesting question Candidates reach for a function value here reflexively, because it is the most flexible thing a language will let them store. The senior move is noticing that flexibility of *execution* was bought with loss of *information*, and asking which of the two the system will need in six months. A queue that can only run its contents is fine until the first time somebody asks what is in it.

  • Why can't a queue of function values simply be written to storage and reloaded after a restart?
    Because the item is code together with whatever it captured, not a value made of fields. Storage holds bytes you can read back into data; nothing in holding a function value gives you a way to reconstruct that code and its captured environment in a fresh process. If the outbox must survive a restart, the item has to be data from the start.
  • Is there a middle ground between a function value and a fully described record?
    Yes: store both in one entry — the callable for execution, plus a small descriptor carrying an identity, a kind and an attempt count. Dedup, ordering and reporting then read the descriptor while execution stays open to any new kind of work. It still does not buy persistence, because the executable half remains non-data.
  • What does the record-plus-interpreter design give up?
    The open set. The interpreter has to recognise every kind, so a new kind of deferred work means editing it, and the queue's code now knows something about the work it carries. That coupling is the price of being able to read, sort, store and explain the items.

saying these in an interview costs you the question

  • Claiming a queue of function values can be written to disk and reloaded
  • Thinking two queued values doing the same work compare equal
  • Saying a function value is strictly more flexible and costs nothing
  • Believing the queue can inspect a value to see whether the work is still needed
  • Expecting a retry to re-run a zero-argument value with different arguments