A corecursive appointment-date producer has no base case, so what condition makes it well defined instead of a definition that spins?
answer
- no base case, still well defined
- output per turn, not termination
- the call sits behind a constructor
- hand back first, then recurse
- a filter branch can starve
basics
~20 sGuardedness: every recursive call must sit behind a step that has already handed back one date, so each turn of the definition delivers output. A path that recurses without delivering anything is the real failure to look for.
solid answer
~50 sThe condition is **guardedness**, sometimes stated as productivity. In a consuming recursion you prove that something shrinks until a base case is reached; in a producer there may be nothing to shrink, so instead you show that each turn of the definition places one element in front of the recursive call. `produce(seed) = prepend(seed.date, produce(advance(seed)))` is guarded: the date exists before the call that makes the rest, so any finite amount of the result is available after finitely many steps. The shape that fails is a branch that recurses **before** producing anything — classically a filter whose predicate does not hold: it advances the seed and hands nothing back, and if no remaining date ever matches, it spins in silence. A producer may still report that it is finished; it just does not need to.
code
pseudocode · 10 lines// guarded: a date exists before the recursive call is reached
function datesFrom(seed)
return prepend(seed.date, datesFrom(advance(seed)))
// unguarded path: the non-matching branch hands back nothing
function datesMatching(rule, seed)
if rule.matches(seed.date) then
return prepend(seed.date, datesMatching(rule, advance(seed)))
else
return datesMatching(rule, advance(seed)) // no element this turngo deeper
Remember that a definition with no base case is not automatically broken. Look for whether each turn hands back one piece of the result before calling itself again; that is what makes an endless generator usable.
Explain guardedness precisely: the recursive call sits inside a constructor that already holds this step's element, so any finite prefix arrives after finitely many steps. Contrast it with a decreasing measure and with tail position.
Diagnose the starving wrapper. Be able to say why a filter over a perfectly good producer can hang, what evidence distinguishes it from a slow source, and what you would require of the predicate before shipping it.
Decide what the codebase guarantees. Whether producers are allowed to be unbounded, whether every filter over one must carry a bound, and whether that rule is enforced by review or by the shape of the published API is a standard someone has to own.
## Termination is the wrong question here Asking whether an endless appointment series terminates is asking a question the definition was never trying to satisfy. The series is supposed to keep going: *every second Tuesday from this date* has no last element by design. What has to be established instead is that the definition **delivers**: after finitely many steps you have the first date, after finitely many more the second, and so on for any finite amount you ask for. That property is **productivity**, and the syntactic condition that buys it is **guardedness**. ## What guardedness means A recursive call is *guarded* when it sits inside a constructor that has already been given the element for this step — the call is a description of the rest, placed behind a piece that already exists. Read the definition one turn at a time: - `produce(seed) = prepend(seed.date, produce(advance(seed)))` — a date is in hand before the inner call is reached, so one turn yields one element. Guarded. - `produce(seed) = let rest = produce(advance(seed)) in prepend(seed.date, rest)` — under an evaluation order that works the binding out first, the inner call must complete before any element exists. Not guarded. - `produce(seed) = produce(advance(seed))` — a turn that yields nothing at all. Not guarded, however busily the seed changes. The last line is the trap worth memorising: **a changing seed is not progress**. Progress for a producer is measured in elements handed back, not in how different the seed looks. ## The filter that starves The realistic failure is not the naked loop above; it is a wrapper. Suppose a base producer hands back every day from an anchor, and a wrapper re-emits only the days matching a rule: 1. The wrapper receives a date from the source. Its non-matching branch advances and calls itself, producing nothing. 2. As long as matches keep arriving eventually, each turn of the *wrapper* still delivers after a bounded number of internal steps, and the whole thing is productive. 3. If no remaining date can ever satisfy the rule — a rule asking for the 30th of February, a range already passed — the wrapper recurses forever and never delivers. The source is still perfectly productive; the composite is not. Guardedness is a property of the definition you actually run, not one inherited from whatever it wraps. That is why a producer plus a predicate deserves more review attention than either piece alone. ## Guarded against unguarded | | guarded step | unguarded step | |---|---|---| | per turn | one element placed before the call | nothing, or an element only on some paths | | what is checked | element exists before recursion is reached | usually nothing is checked | | failure mode | none from this cause | spins silently, producing no output | | typical shape | element placed in front of the rest | a filter branch, or a plain re-call | | observable symptom | first element arrives promptly | the first element never arrives | ## Reviewing a producer in four moves 1. Find every path through the step, including the ones that only run for certain rule values. 2. On each path, ask whether an element is in hand before the recursive call is reached. 3. For any path that emits nothing, ask whether a path that does emit is guaranteed to be reached within a bounded number of steps. If that cannot be argued, the definition can starve. 4. Decide whether the producer should be able to report *no more*. A bounded rule holds its remaining count in the seed and stops; an unbounded one does not, and both are legitimate. ## A word about what guardedness is not It is not tail position: a guarded call is deliberately **not** the last thing that happens, because a constructor is applied around it. It is not purity: a self-contained step can still spin. It is not a promise that anything finishes; it is a promise that the next piece arrives. Getting those apart is most of the answer, and stating them backwards — *recurse, then emit* — is the one mistake that makes the whole definition worthless while still reading fluently.
- Why can a filter placed over a productive producer stop being productive?Because its non-matching branch recurses without handing anything back. The source still supplies a date per step, but the wrapper only turns that into output when the rule holds; if no remaining date can satisfy the rule, the wrapper advances forever in silence. Guardedness belongs to the definition you run, not to the one it wraps.
- Does a producer that stops after ten occurrences still count as corecursive?Yes. The step may report that there are no further elements, with the remaining count carried in the seed. Corecursion describes how a structure is built — one piece at a time, outward from a seed — so whether the pieces happen to run out is a property of that particular step, not of the style.
- Is a guarded call the same as a tail call?No, and usually the opposite. A guarded call is wrapped in a constructor, so something happens after it returns and it is deliberately not in tail position. Tail position is about reusing the current frame; guardedness is about delivering an element before recursing. A definition can have either, both or neither.
saying these in an interview costs you the question
- Says a recursion without a base case is always wrong
- Confuses productivity with the call being in tail position
- Believes any endless producer is automatically productive
- Checks only that the seed changes on every step
- Assumes a filter inherits the source producer's guarantee
- States the rule backwards as recurse first, then emit