skip to content

In Ruby, how would you generate paid?, shipped? and cancelled? predicate methods on an Order class from a list of its statuses?

level: juniorimportance: should knowfreq 42%

answer

  1. iterate a frozen list
  2. interpolated Symbol as the name
  3. each gives a fresh variable
  4. for shares one variable
  5. every predicate checks the last status

basics

~10 s

Loop over a frozen STATUSES list with each and call define_method(:"#{s}?") { status == s } for every entry. Each iteration's block captures its own s, so every predicate compares against its own status.

solid answer

~40 s

I keep the statuses in a frozen constant and generate the predicates in the class body: `STATUSES.each { |s| define_method(:"#{s}?") { status == s } }`. The name is built by interpolation, and the method body is a closure over `s`. Because `each` gives every iteration a fresh block parameter, `paid?` keeps `:paid` and `shipped?` keeps `:shipped`. Writing the same thing with a `for` loop is a classic bug: `for` does not open a new scope, so all the closures share one variable and every predicate ends up comparing with the last status. I would add a test that checks each predicate against every status.

code

ruby · 13 lines
ruby
class Order
  STATUSES = %i[pending paid shipped cancelled].freeze
  attr_reader :status

  def initialize(status) = @status = status

  STATUSES.each do |s|
    define_method(:"#{s}?") { status == s }
  end
end

Order.new(:shipped).shipped?  # => true
Order.new(:shipped).paid?     # => false

go deeper

for a junior

Recall the shape: each over a list, an interpolated name, and define_method with a block that compares the status.

for a middle

Explain why each iteration keeps its own value while a for loop shares one variable, and what that does to the predicates.

for a senior

Show that you test generated families against the full list and keep them discoverable for readers who search for def.

for a principal

Decide when a generated family earns its keep over writing a handful of methods by hand.

## The task An `Order` has a `status` that is one of a fixed set: `pending`, `paid`, `shipped`, `cancelled`. Callers want to write `order.paid?` rather than `order.status == :paid`. Writing four near-identical methods by hand works, but the list grows, and every new status needs a matching method that someone may forget. **Generating** the predicates from the list keeps the two in step. ## The idiomatic version ```ruby class Order STATUSES = %i[pending paid shipped cancelled].freeze attr_reader :status def initialize(status) = @status = status STATUSES.each do |s| define_method(:"#{s}?") { status == s } end end Order.new(:paid).paid? # => true Order.new(:paid).shipped? # => false ``` How it works, step by step: 1. `STATUSES.each` runs the block once per status, while the class body is being executed. 2. `:"#{s}?"` builds the method name, such as `:paid?`. A String would work too. 3. `define_method` adds an instance method with that name. Its body is the inner block. 4. The inner block is a **closure** over `s`. Because `each` gives every iteration a **fresh** block parameter, each generated method keeps its own `s`. 5. Inside the method body, `self` is the order, so `status` calls the reader. The generated methods are ordinary public instance methods: `Order.instance_methods(false)` lists them, and `Order.new(:paid).respond_to?(:paid?)` is `true`. ## The loop that breaks it The same code with a `for` loop looks equivalent and is not: ```ruby for s in STATUSES define_method(:"#{s}?") { status == s } end Order.new(:paid).paid? # => false Order.new(:cancelled).paid? # => true ``` A `for` loop does **not** create a new variable scope. There is one `s`, a local of the class body, and every iteration assigns to it. All four closures capture that **same** variable, so after the loop they all compare against its final value, `:cancelled`. The method names are right (they were computed during each iteration), but every body answers the question "is it cancelled?". The same trap appears with `while` loops or with a counter updated outside the block. | Loop | Variable per iteration | Result | |---|---|---| | `STATUSES.each { \|s\| ... }` | fresh `s` each time | each predicate checks its own status | | `for s in STATUSES` | one shared `s` | every predicate checks the last status | ## Details worth getting right - **Freeze the list.** `STATUSES` is read by the generator and usually by validations; freezing it stops a caller from adding a status that has no predicate. - **Keep the closure small.** The block captures the whole local scope of the class body, not just `s`. Avoid defining large temporary objects as locals in the same scope. - **Mind collisions.** A generated name can overwrite a method of the same name defined earlier in the class; generate from a list you control. - **Document the family.** A text search for `def paid?` finds nothing. A comment above the loop listing the generated names, or a test that checks every status has its predicate, helps the next reader. ## Alternatives and their trade-offs - **Hand-written methods.** For three or four statuses that rarely change, plain `def paid? = status == :paid` lines are the most readable and are found by any search. - **A single query method.** `order.status?(:paid)` avoids generation entirely, at the cost of a less fluent call site. - **Generation with `define_method`.** Best when the list is long, changes often, or already drives validations and user interface choices. ## Testing the family ```ruby Order::STATUSES.each do |s| order = Order.new(s) raise "#{s}? wrong" unless order.public_send(:"#{s}?") others = Order::STATUSES - [s] raise "#{s} leaks" if others.any? { order.public_send(:"#{it}?") } end ``` A loop like this catches the `for` bug at once, because it checks each predicate against every status. ## What an interviewer is listening for A junior answer shows `each` plus `define_method` with an interpolated name. A good answer explains why each method keeps its own status value, and a very good one volunteers the `for` loop failure without being asked.

  • Why does the method name come out right even in the broken for-loop version?
    The name is computed immediately, while `define_method` is being called in that iteration, so `:"#{s}?"` sees the current value. Only the body is deferred: it reads `s` later, when the method is called, and by then the single shared variable holds the last status.
  • How would you make the generated predicates private?
    Call `private` before the loop in the class body; `define_method` running there picks up the default visibility, just like `def`. Alternatively pass the names to `private` after the loop, since `private` accepts a list of method names.

Each iteration of each hands a different envelope to each new method, while a for loop hands them all a photocopy of the same whiteboard that keeps being rewritten until the loop ends.

saying these in an interview costs you the question

  • a for loop and each capture loop values the same way
  • each closure copies the variable's value when it is created
  • define_method needs a String name, so a Symbol must be converted
  • the predicates are generated every time Order.new runs
  • generated methods do not appear in instance_methods