skip to content

Predicates & Selection

any?, all?, none? and one? answer yes-or-no questions, while find, select, reject, grep, partition and filter_map pull elements out. Interviewers probe pattern arguments and no-match results.

on this pageshow

explore

questions

5

In Ruby, how do find and select differ when looking for overdue invoices, and what does each return when nothing matches?

level: juniorimportance: must knowfreq 68%

answer

  1. one element or many
  2. find stops at the first hit
  3. nil on a miss, not an error
  4. select returns [] when empty
  5. find(ifnone) takes a callable

basics

~20 s

find (alias detect) returns the first element whose block is truthy and stops there, or nil when none matches. select (alias filter) walks the whole collection and returns an array of every match, empty when none.

solid answer

~40 s

`find` and its alias `detect` answer "which is the first one?": they yield elements until the block returns a truthy value, return that **element**, and stop. On a miss they return `nil`, unless you pass an *ifnone* callable, as in `find(-> { raise NotFound })`, whose result is returned instead. `select`, with aliases `filter` and `find_all`, answers "which ones?": it always walks every element and returns a new **array**, `[]` when nothing matches. `reject` is its complement, and `find_index` returns the position rather than the element. The practical traps are calling a method on `find`'s result without handling `nil`, and writing `select { ... }.first`, which builds a full array only to take its first element.

code

ruby · 17 lines
ruby
Invoice = Data.define(:number, :days_late)
invoices = [
  Invoice.new(number: "INV-7", days_late: 0),
  Invoice.new(number: "INV-9", days_late: 45),
  Invoice.new(number: "INV-12", days_late: 70)
]

invoices.find { |i| i.days_late > 30 }&.number    # => "INV-9"
invoices.select { |i| i.days_late > 30 }.map(&:number)
# => ["INV-9", "INV-12"]

invoices.find { |i| i.days_late > 90 }            # => nil
invoices.select { |i| i.days_late > 90 }          # => []
invoices.find_index { |i| i.days_late > 30 }      # => 1

invoices.find(-> { raise KeyError, "none 90+ days late" }) { |i| i.days_late > 90 }
# KeyError: none 90+ days late

go deeper

for a junior

Recall that find returns the first matching element or nil, and select returns an array of all matches, empty when none.

for a middle

Explain the aliases detect, filter and find_all, the ifnone argument to find, and why find returns the element rather than the block's value.

for a senior

Flag select.first and unguarded calls on find's result in review, and decide per call site whether a miss is nil or an error.

for a principal

Encourage a team convention for absence: nil-returning finders for optional data, raising finders where a miss means a broken invariant.

## Two questions, two methods An accounts-receivable screen holds a list of invoices, and two different questions come up constantly: - **"Is there an overdue invoice, and which is the first?"** That is `find`. - **"Which invoices are overdue?"** That is `select`. Both come from Ruby's `Enumerable` module, which `Array`, `Hash`, `Range` and any class that includes it and defines `each` can use. `Array` has its own faster versions of several of them, with the same behaviour; Ruby 4.0 added a native `Array#find` for that reason. ## `find` and `detect` `find` calls the block with each element in turn. The first time the block returns a **truthy** value (anything except `nil` and `false`), `find` returns **that element** and stops iterating. - `detect` is the same method under another name; the two are defined from one C function. - On a miss, `find` returns **`nil`**. It does not raise. - It accepts an optional **ifnone** argument: any object that responds to `call`, typically a lambda. If nothing matches, `find` calls it and returns its result. ```ruby invoices.find(-> { raise NoOverdueInvoice }) { |inv| inv.overdue? } ``` That is the idiom when "none found" is an error rather than a normal outcome. ## `select`, `filter` and `find_all` `select` calls the block for **every** element and returns a **new array** of the ones for which it was truthy. `filter` and `find_all` are aliases. On a miss the result is `[]`, never `nil`, so the caller can iterate it without a guard. `reject` is the complement: it keeps the elements for which the block is falsy. ## Side by side | | `find` / `detect` | `select` / `filter` | `find_index` | |---|---|---|---| | Returns | one element | array of elements | an Integer position | | On no match | `nil` (or ifnone's result) | `[]` | `nil` | | Stops early | yes, at the first match | no, walks everything | yes, at the first match | | Argument form | ifnone callable | none | a value compared with `==` | ## Handling the `nil` Most bugs around `find` are about the `nil` it returns on a miss: 1. `invoices.find { |i| i.overdue? }.amount` raises `NoMethodError` when nothing is overdue. 2. The safe navigation operator, `find { ... }&.amount`, turns that into `nil`, which is right only if the caller expects a missing value. 3. The ifnone lambda, or an explicit `|| raise`, makes the absence an error at the point where it happens. A related subtlety: `find` returns the **element**, not the block's value. If you want the first non-nil *result* of a computation, `find` is the wrong tool; that is a mapping question. ## Truthy, not `true` Both methods test the block's result for **truthiness**, not for the value `true`. Only `nil` and `false` count as a miss; every other object, including `0` and `""`, counts as a match. Two practical consequences: - A block that returns a field, such as `find { |i| i.disputed_at }`, matches the first invoice whose field is set, whatever its value. That is sometimes exactly what you want, and sometimes a bug when the field was meant to be compared with something. - A block whose last line is a debugging `puts` returns `nil`, so `select` suddenly returns `[]` and `find` returns `nil`. Nothing raises; the filter simply stops matching. Reading the block's **last expression** is therefore the first step when a `find` or `select` returns nothing unexpectedly. ## Why `select { ... }.first` is a smell `invoices.select { |i| i.overdue? }.first` gives the same answer as `find`, but it: - calls the block on every invoice, even after the first match; - allocates an array of all matches only to discard most of it; - runs any side effects in the block more times than needed. For ten invoices nobody notices. For a large list, or a streaming source that must be read to the end, the difference is real, and reviewers flag it. ## `find_index` `find_index` is the positional cousin of `find`. With a block it returns the index of the first element for which the block is truthy; with an argument it returns the index of the first element `==` to that value. Both return `nil` on a miss. On arrays it is the same method as `Array#index`. ## What the interviewer wants State the return types first: one element or `nil` versus an array, possibly empty. Then show you know the consequences: `nil` handling around `find`, the ifnone argument, and preferring `find` over `select.first`. Mentioning that `detect` and `filter` are aliases shows you can read code written in either vocabulary.

  • How do you make find raise when nothing matches?
    Pass a callable as its argument: `invoices.find(-> { raise NotFound }) { |i| i.overdue? }`. `find` calls the argument only when no element matched and returns its result, so a lambda that raises turns a miss into an error. Writing `find { ... } || raise(NotFound)` also works, as long as no element can itself be `nil` or `false`.
  • Why prefer find over select followed by first?
    `find` stops at the first truthy block result, while `select` evaluates the block for every element and allocates an array of all matches. Both return the same element, but `select.first` does more work, runs block side effects more often, and on a streaming source reads everything before answering.

saying these in an interview costs you the question

  • find raises an exception when no element matches
  • select returns nil when nothing matches
  • detect raises on a miss, unlike find
  • find returns the value of the block, not the element
  • select { ... }.first stops at the first match like find
open as a page

In Ruby, what do any?, all?, none? and one? return on an empty collection, and what changes when you pass them a pattern?

level: middleimportance: must knowfreq 55%

basics

~20 s

On an empty collection all? and none? return true; any? and one? return false. A pattern argument tests pattern === element, so classes, ranges and regexps work, and a block passed alongside is ignored with a warning.

open as a page

In Ruby, how do grep and grep_v filter an invoice list by class, range or regexp, and what does grep with a block return?

level: middleimportance: should knowfreq 32%

basics

~20 s

grep(pattern) returns the elements for which pattern === element, and grep_v returns the rest. The pattern can be a class, range, regexp or plain value. With a block, grep returns the block's results for the matching elements, filtering and mapping in one pass.

open as a page

In Ruby, when do partition and filter_map beat chaining select with reject or map, and which values does filter_map drop?

level: middleimportance: should knowfreq 38%

basics

~20 s

partition splits a collection in one pass into [matching, rest]. filter_map runs a block once per element and keeps its truthy results, dropping both nil and false, so it replaces a map followed by compact or a select followed by map.

open as a page

In Ruby, scanning a due-date-sorted list of millions of invoices, which query methods stop early, and why is take_while not a drop-in for select?

level: seniorimportance: should knowfreq 28%

basics

~20 s

find, any?, all?, none?, include? and take_while stop once the answer is known; select, reject, count, grep, partition and filter_map walk everything. take_while stops at the first failure, so it matches select only on data ordered by that test.

open as a page