In Ruby, how do find and select differ when looking for overdue invoices, and what does each return when nothing matches?
answer
- one element or many
- find stops at the first hit
- nil on a miss, not an error
- select returns [] when empty
- find(ifnone) takes a callable
basics
~20 sfind (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 linesInvoice = 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 latego deeper
Recall that find returns the first matching element or nil, and select returns an array of all matches, empty when none.
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.
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.
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