In Ruby, how would you write an export method that accepts anything responding to each, and what happens when a caller passes something else?
answer
- call each, don't check the class
- Array, Set, Hash, Enumerator all work
- a plain class with each works too
- NoMethodError names method and receiver
- 'for nil' vs 'for an instance of'
basics
~20 sCall rows.each without a class check, so Arrays, Sets, Hashes, Enumerators and any object defining each all work. Anything else fails with NoMethodError, such as undefined method 'each' for an instance of Integer, or for nil.
solid answer
~40 sDuck typing means the method depends on a **capability**, not a class: `def export(rows) = rows.each { |row| ... }`. An `Array`, a core `Set`, a `Hash` (which yields key/value pairs), an `Enumerator` such as `rows.lazy.select { ... }`, and any class that defines `each` all work, while an `is_a?(Array)` guard would have rejected all but the first. When a caller passes something without `each`, Ruby raises `NoMethodError` at the call; in Ruby 4.0 the message reads `undefined method 'each' for an instance of Integer`, or `undefined method 'each' for nil` when a nil slipped through. `NoMethodError` is a `NameError` and carries `name`, `receiver` and `args`, so the failure points at the caller's bad argument. A `String` is a common surprise: it has no `each`, so it fails the same way.
code
ruby · 24 linesdef export(rows)
lines = []
rows.each { |row| lines << row.join(",") }
lines.join("\n")
end
class Report
def each
yield ["id", "total"]
yield [1, 9.99]
end
end
export([[1, "a"]]) # => "1,a"
export(Set[[2, "b"]]) # => "2,b" (Set is core in Ruby 4.0)
export({3 => "c"}) # => "3,c"
export(Report.new) # => "id,total\n1,9.99"
begin
export(nil)
rescue NoMethodError => e
e.message # => "undefined method 'each' for nil"
[e.name, e.receiver] # => [:each, nil]
endgo deeper
Recall that calling each without a class check lets Arrays, Sets, Hashes and custom classes through, and that anything else raises NoMethodError.
Explain how NoMethodError's name and receiver identify the bad argument, and why using map instead of each narrows the accepted duck type.
Trace a NoMethodError for nil back to where the nil was produced, and document duck-typed contracts so callers know the required capability.
Set conventions for how public APIs state and enforce duck-typed contracts, balancing flexibility against clear, early errors for callers.
## Depend on what an object can do **Duck typing** is Ruby's default style: a method uses the methods it needs and accepts any object that has them. An exporter that writes rows needs exactly one capability from its input, a way to iterate, so it calls `each` and nothing else: ```ruby def export(rows) lines = [] rows.each { |row| lines << row.join(",") } lines.join("\n") end ``` The method never asks what class `rows` is. That single decision is what lets very different objects through. ## What passes | Argument | Why it works | |---|---| | `[[1, "a"], [2, "b"]]` | `Array#each` yields each row | | `Set[[1, "a"]]` | `Set` is a core class in Ruby 4.0 with its own `each` | | `{1 => "a"}` | `Hash#each` yields `[key, value]` pairs | | `[[1, "a"]].each` or `rows.lazy.select { ... }` | both return an `Enumerator`, which has `each` | | a `Report` object whose class defines `each` | it has the one method the exporter calls | The `Report` class does not even need to include `Enumerable`; defining `each` is enough for this method. (Including `Enumerable` would add `map`, `select` and friends for callers that want them.) A guard such as `raise ArgumentError unless rows.is_a?(Array)` would reject every row but the first, even though all of them would have worked. ## What fails, and how When an object lacks `each`, the call itself raises **`NoMethodError`**: 1. `export(42)` raises `undefined method 'each' for an instance of Integer`. 2. `export(nil)` raises `undefined method 'each' for nil`, the most common real-world case, usually from a missing Hash key or an unset variable upstream. 3. `export("1,a")` raises the same error for a String: `String` has `each_line` and `each_char` but no `each`. The message format changed recently: since Ruby 3.3 it says "for an instance of Integer" instead of printing the receiver's `inspect`, and since Ruby 3.4 it uses a single quote instead of a backtick before the method name. Older blog posts quoting `undefined method `each' for nil:NilClass` show the pre-3.3 form. ## Reading the exception `NoMethodError` is a subclass of **`NameError`**, and both are `StandardError`s. Its accessors make a failure easy to attribute: - `name` returns the missing method as a Symbol, here `:each`; - `receiver` returns the object that lacked it, here `42` or `nil`; - `args` returns the arguments of the failed call. In the backtrace, the top frame is inside `export`, but the cause is almost always the caller that passed the wrong object. Following the value back to where it was produced (often a `nil` from a lookup) is the real fix. ## Making the contract visible Duck typing does not mean undocumented. Good practice for a method like this: - Name the capability in the parameter documentation: "rows: any object that responds to `each` and yields Arrays". - Keep the requirement minimal. Calling `rows.map` instead of `rows.each` would silently raise the bar to `Enumerable` and reject a bare `Report` with only `each`. - Let `NoMethodError` surface; do not rescue it inside `export`, where it could also hide a genuine bug in the row formatting. - If an early, friendlier error is worth it at a public API boundary, check the capability with `respond_to?(:each)`, not the class. ## Testing a duck-typed method Tests should prove the method depends on the capability, not the class: - pass an Array and a Set and assert the same output; - pass a tiny class that defines only `each`, which catches an accidental switch to `map`; - pass `nil` and assert a `NoMethodError` whose `name` is `:each`, documenting the failure mode on purpose. ## Why this is idiomatic Ruby's core library is built the same way: `Enumerable` needs only `each`, `Comparable` needs only `<=>`, and methods like `puts` accept anything with `to_s`. Code that follows the pattern composes with objects its author never saw, including test doubles, lazy enumerators and decorators.
- Why would switching the body from `rows.each` to `rows.map` narrow what the export method accepts?`map` comes from `Enumerable` (and Array's own implementation), not from `each` itself. A class that defines only `each` without including `Enumerable` has no `map`, so it would now fail with NoMethodError. Using the smallest method you need keeps the duck type as wide as possible.
- Why is rescuing NoMethodError inside `export` a bad way to report a wrong argument?The rescue would also catch NoMethodErrors raised while formatting a row, such as a nil inside the data, and report them as "wrong argument". If you must translate the error, check that `e.receiver.equal?(rows)` and `e.name == :each` before re-raising a friendlier one; otherwise re-raise the original.
saying these in an interview costs you the question
- The export method should check rows.is_a?(Array) before iterating.
- A String responds to each, yielding its lines.
- Passing nil makes each silently yield nothing.
- An object must include Enumerable before anything can call its each.
- Ruby 4.0 reports undefined method `each' for nil:NilClass.