In a job-board candidate filter, what does an 'and' combinator over two named predicates return - a boolean or another predicate?
answer
- the result is still callable
- a condition built, not evaluated
- two predicates in, one predicate out
- no candidate exists at assembly time
- the operator joins booleans, the combinator joins functions
basics
~10 sAnother predicate. A combinator builds a condition rather than evaluating one: nothing is tested until the assembled predicate is finally applied to a candidate, and then it asks both questions and combines the answers.
solid answer
~40 sA predicate is a function from one candidate to true or false. A predicate combinator takes predicates and hands back a predicate, so `and(hasWorkPermit, seniorEnough)` produces a new function; no candidate exists at the point that line runs. The confusion comes from the name: the boolean operator combines two `true`/`false` values, while the combinator combines two functions that will later produce such values. Because the result is itself a value, you can name it, store it, pass it to whatever runs the filter, and apply it to a second list without rewriting anything. Evaluation happens once per candidate, at the moment the assembled condition is applied.
code
pseudocode · 8 linesfunction and(left, right)
return function(candidate)
return left(candidate) and right(candidate)
employable = and(hasWorkPermit, seniorEnough)
// nothing has been tested yet; now it is
shortlist = filter(applicants, employable)go deeper
Remember the shape: predicates in, predicate out. Be able to say that no candidate is examined at the moment the filter is assembled, and that the parts run later, once per candidate.
Explain the two moments - assembly and application - and why having the condition as a value lets you store it, pass it, and apply it to a second list without rewriting the logic.
Show where you drew the line in a real codebase: which conditions earned named predicates, which stayed inline, and how you kept the vocabulary from drifting as the filter grew.
Frame it as a cost: an extra naming layer bought testability and run-time assembly. Say when that trade is not worth making for a team, and what convention keeps the predicate vocabulary honest.
## A predicate, and then a combinator A **predicate** is a function that takes one value and answers a yes/no question about it: `hasWorkPermit(candidate)`, `seniorEnough(candidate)`. A **predicate combinator** is a function whose arguments are predicates and whose result is another predicate. When a job board's filter is written as `and(hasWorkPermit, seniorEnough)`, that expression does not look at a candidate - there is no candidate in scope when it runs. It yields a new function which, once it is handed a candidate, asks both questions and combines the two answers. That single distinction is the whole of the beginner version of this subject, and the mistake it guards against is reading `and` as the boolean operator it borrowed its name from. The operator combines two **booleans**. The combinator combines two **functions that produce booleans**. ## The two shapes side by side | | Inline boolean expression | Combinator | |---|---|---| | What you write | `candidate.permit and candidate.years >= 3` | `and(hasWorkPermit, seniorEnough)` | | What it evaluates to | `true` or `false` | a predicate | | When the test runs | immediately, for the one candidate in scope | later, once per candidate it is applied to | | What you can store | only the resulting boolean | the condition itself | | Are the parts named | only if you extract a variable or a function | each part is already a named value | ## Why "returns a predicate" is the property that matters - **The condition becomes a value.** It can be returned from a function, put in a list, passed to whatever walks the candidate list, and applied to a second list later. - **Each part is independently testable.** `seniorEnough` can be exercised on its own; a clause buried inside a long boolean expression cannot be, except through the whole expression. - **The pieces are reusable across filters.** The same `hasWorkPermit` appears in the search filter, in the alerting rule and in the export, and it means the same thing in all three because it is one definition. - **Assembly can be data-driven.** If the parts are values, the set of parts can be chosen at run time rather than written out as a branch per combination. - **Reading order matches intent.** `and(hasWorkPermit, seniorEnough)` says what is required; a nested chain of field comparisons says how it is checked. ## Assembly time and test time These are two different moments, and keeping them apart is what makes the rest of this material make sense: 1. **Assembly.** The combinators run. `and`, `or` and `not` are applied to predicates and produce one predicate. Zero candidates are examined. This usually happens once. 2. **Application.** The assembled predicate is applied to a candidate. Now the parts run, their booleans are combined, and one answer comes out. 3. **Repetition.** The same assembled predicate is applied to the next candidate, and the next. The assembly step is not repeated. A useful sanity check when reading unfamiliar code: ask what the expression would do if there were no candidates at all. If the answer is "nothing, it just produces a condition", it is a combinator. If the answer is "it would not compile, there is no value to test", it is an ordinary boolean expression. ## The three basic combinators The usual starting set is small, and every larger one is built from it: - **Conjunction** - a predicate that holds when both parts hold. - **Disjunction** - a predicate that holds when at least one part holds. - **Negation** - a predicate that holds exactly when its part does not. Most codebases also grow list forms - one that requires every predicate in a collection to hold, and one that requires at least one - because the number of criteria is usually discovered at run time rather than known when the code is written. ## What the style costs It is not free, and an interviewer will respect a candidate who says so: - **A layer of indirection.** Reading the filter now means looking up what each named predicate does, instead of seeing the comparison inline. - **Overkill for a one-off.** A single two-clause condition used in exactly one place is clearer written out. - **A vocabulary to maintain.** Named predicates only pay off if names are accurate; a `seniorEnough` whose definition quietly drifts is worse than the comparison it replaced. The trade is the usual composition trade: you accept one more level of naming in exchange for parts that can be tested, reused and assembled by something other than a programmer typing out every combination.
- What must the two predicates agree about for a conjunction combinator to make sense?They must ask about the same kind of subject. Both take a candidate, so the combined predicate can hand the one candidate it receives to each part. Combining a predicate over candidates with a predicate over job postings has nothing to pass to both, which a statically checked language rejects outright and a dynamically checked one discovers at the first application.
- If the combinator returned a boolean instead of a predicate, what would a caller have to do to filter a second list?Rewrite the condition at the second call site, because the only thing it received was one answer about one candidate. Returning a predicate means the condition itself is the reusable artifact: the second list is filtered by passing the same value somewhere else, with no duplicated logic to drift apart.
Combining two recipe cards gives you a third recipe card, not a cooked meal. The cooking only happens when someone finally takes the card into the kitchen with actual ingredients.
saying these in an interview costs you the question
- Says the combinator answers true or false straight away
- Thinks the named predicates run when the filter is assembled
- Cannot say what argument the combined predicate expects
- Treats named predicates as purely cosmetic sugar for one expression
- Assumes building conditions this way needs a class hierarchy