skip to content

In a job-board candidate filter, what does an 'and' combinator over two named predicates return - a boolean or another predicate?

level: juniorimportance: must knowfreq 62%

answer

  1. the result is still callable
  2. a condition built, not evaluated
  3. two predicates in, one predicate out
  4. no candidate exists at assembly time
  5. the operator joins booleans, the combinator joins functions

basics

~10 s

Another 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 s

A 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 lines
pseudocode
function 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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