A job board lets a recruiter tick any subset of filter criteria; how do you assemble the matching condition at run time?
answer
- criteria become data, not branches
- one named predicate per checkbox
- fold the chosen list into one condition
- the empty selection must accept everyone
- sort by cost before folding, not after
basics
~20 sMap each ticked criterion to its named predicate, collect the chosen ones into a list, and fold the list into a single condition with the conjunction combinator. Ticking nothing folds to a condition that accepts every candidate.
solid answer
~40 sBecause a combinator returns a predicate rather than a boolean, the set of criteria can be data instead of code. Keep one named predicate per criterion, look up the ones the recruiter ticked, and fold that list with conjunction into one assembled condition applied to each candidate. The alternative - a branch per combination - grows as two to the power of the number of criteria, so six checkboxes would mean 64 hand-written conditions. Two details make or break it: an empty selection must fold to a condition that accepts everything, not one that rejects everything, and the list is in checkbox-declaration order, so if the parts differ wildly in cost you sort them before folding rather than trusting that order.
code
pseudocode · 8 linesfunction allOf(predicates)
condition = acceptEverything // empty selection accepts all
for each p in predicates
condition = and(condition, p)
return condition
chosen = lookUpEach(request.criteria) // known names only
shortlist = filter(applicants, allOf(sortByCost(chosen)))go deeper
Know the shape: each checkbox has its own small predicate, the chosen ones go into a list, and the list is combined into one condition applied per candidate.
Explain why branching does not scale - combinations double per criterion - and state both empty-fold values: accept-everything for conjunction, accept-nobody for disjunction.
Cover the operational edges: validating criterion names against a known set, sorting the list by cost before folding, and logging the effective filter that was applied.
Treat the predicate vocabulary as a shared asset across search, saved alerts and export, and decide who may add to it and how a criterion's cost and selectivity are recorded.
## The problem the style solves A recruiter sees a panel of checkboxes: has a work permit, three or more years, open to relocation, holds a named certification, available within a month. Any subset can be ticked. Written as branching code, the number of distinct conditions is **two to the power of the number of criteria** - 5 checkboxes give 32 combinations, 6 give 64 - and every new checkbox doubles it. Nobody writes that; they write one long conditional with a null-check per criterion, which is the same explosion in a shape that hides it. Predicate combinators turn the explosion into a **list**. Because combining predicates yields a predicate, the criteria are values, and values can be selected, filtered and folded at run time. ## The assembly, step by step 1. **One named predicate per criterion.** `hasWorkPermit`, `seniorEnough`, `openToRelocation` - each one small, individually testable, and meaningful on its own. 2. **A lookup from criterion to predicate.** The request names criteria; the lookup turns those names into the predicates they stand for. Unknown names are rejected here, not passed through. 3. **Select the chosen ones** into a list, in whatever order you decide is right (see below). 4. **Fold the list into one predicate** with the conjunction combinator - start from the accept-everything condition and combine each element into it. 5. **Apply the result once per candidate.** The filtering code never learns how many criteria exist or which ones the recruiter picked. It receives one condition. ## The empty selection The case that gets shipped wrong is the empty list. Folding an empty list of parts has to produce **the condition that accepts every candidate**, because "no criteria were requested" means "do not narrow the results". Get the starting value backwards and an untouched filter panel returns zero results - a bug that looks to the user like the job board is broken and looks to the developer like an empty data set. The mirror case matters too: folding an empty list under **disjunction** has to produce the condition that accepts nobody, because "at least one of no requirements holds" is never satisfiable. The two starting values are opposite, and copying the fold from one combinator to the other without flipping it is how the defect actually arrives. ## What you gain - **Adding a criterion is one entry**, not a rewrite of a conditional. - **Each criterion is unit-testable alone**, against a handful of candidates, with no filter panel involved. - **The assembled condition is inspectable.** If each part carries a name, the effective filter can be logged or shown back to the user as the set of criteria actually applied. - **The order of parts becomes tunable data.** Since the list exists as a value before it is folded, it can be sorted by estimated cost or rejection rate rather than by the order the checkboxes happened to be declared in. - **The same vocabulary serves other features** - saved searches, alerting rules, exports - because each is a different selection over the same set of named predicates. ## What to watch for - **Names from a request are not predicates.** Accepting an arbitrary string and turning it into a condition - a field path, an expression, an operator - hands the caller control of what the filter does. Map only known names to predicates you wrote, and refuse the rest. - **Cost order is now accidental.** A hand-written condition had a deliberate sequence; a folded list has the order the criteria arrived in. If one part is far more expensive than the others, sort before folding. - **A criterion with parameters is a predicate factory, not a predicate.** "At least N years" is a function from N to a predicate; the lookup returns the factory and the request supplies N. - **Combining the wrong way round.** Some panels mean "all of these" and some mean "any of these", and a few mean "all of these groups, any within a group". Decide per panel, and nest the combinators rather than flattening them into one list. The interview value of this question is that it moves the topic from a definition to a design: the reason a combinator returns a predicate is precisely so the set of conditions can be decided by something other than the programmer typing them out.
- What should the assembled condition be when the recruiter ticks nothing?The condition that accepts every candidate, so an untouched panel does not narrow the results. Under disjunction the empty case is the opposite - a condition that accepts nobody - because no requirement can be satisfied by an absent list. Copying one fold's starting value into the other combinator is the usual source of an empty result page.
- What is the risk in letting the request name criteria directly?Only if the names are looked up against a fixed set of predicates you wrote. Accepting arbitrary field paths, operators or expressions from a request lets a caller define the filter, which is a data-exposure problem rather than a design question. Map known names, reject the rest, and report unknown names as a bad request.
- How do you support a criterion that carries a value, like 'at least N years'?Keep a function from the value to a predicate rather than a predicate. The lookup returns the factory, the request supplies N after validation, and the resulting predicate joins the list like any other. The fold never learns that some entries were parameterised.
saying these in an interview costs you the question
- Writes a branch per combination of checkboxes
- Folds an empty selection into a condition rejecting everyone
- Reuses the conjunction starting value for a disjunction fold
- Builds the condition from raw field names in the request
- Assumes checkbox declaration order is a sensible evaluation order
- Flattens 'all of these groups, any within a group' into one list