In Ruby, when is an explicit is_a? check still the right choice over duck typing, and what do class checks break?
answer
- overlapping duck types need a class
- validate untrusted input at the edge
- other.is_a?(self.class) in comparisons
- proxies fail is_a?
- check a module, not a concrete class
basics
~20 sCheck the class when duck typing cannot tell shapes apart (one Hash vs an Array of rows, both with each), when validating parsed input, or in comparisons with other. Class checks reject proxies, decorators and custom collections that would work.
solid answer
~40 sDuck typing fails when two accepted shapes share the same methods: an export that takes a single row Hash or an Array of rows cannot use `respond_to?(:each)`, because both respond. There `rows = [rows] if rows.is_a?(Hash)` is honest. Class checks also belong at **boundaries**, where parsed JSON may hold `"5"` instead of `5` and `is_a?(Integer)` plus a clear `TypeError` beats a failure deep inside, and in comparison methods, where `other.is_a?(self.class)` keeps `<=>` from calling methods on unrelated objects. The cost: a check on a concrete class rejects `Set`s and `Enumerator`s when you test for `Array`, and a `SimpleDelegator` wrapping an Array answers `false` to `is_a?(Array)` while forwarding `each`. When you must check, prefer the widest meaningful type, such as `Enumerable` rather than `Array`, and use `is_a?` rather than `instance_of?`.
code
ruby · 17 linesrequire "delegate"
def export(rows)
rows = [rows] if rows.is_a?(Hash) # one row or many: shapes overlap
lines = []
rows.each { |row| lines << row.values.join(",") }
lines.join("\n")
end
export({id: 1, total: 9}) # => "1,9"
export([{id: 1, total: 9}, {id: 2, total: 5}])
wrapped = SimpleDelegator.new([[1, 2]])
wrapped.respond_to?(:each) # => true, forwarded
wrapped.is_a?(Array) # => false, the wrapper's class
Set[1].is_a?(Enumerable) # => true
Set[1].is_a?(Array) # => falsego deeper
Recall that Ruby prefers calling methods over checking classes, and that is_a? is the usual form when a class check is needed.
Explain overlapping duck types, such as a Hash and an Array of Hashes both having each, where only a class check tells them apart.
Place class checks at trust boundaries with clear TypeError messages, check the widest type, and anticipate delegators and Enumerators failing concrete checks.
Set API design rules that avoid overlapping shapes in the first place, and decide where validation lives so class checks stay at the edges.
## Duck typing first, but not always Idiomatic Ruby asks objects for **capabilities** by calling their methods. Explicit class checks with `is_a?` are the exception, and interviewers want to hear when that exception is justified and what it costs. ## Case 1: overlapping duck types Some methods accept more than one shape, and the shapes answer to the same methods. An exporter that takes either one row as a Hash or many rows as an Array of Hashes is the classic example: - `{id: 1}.respond_to?(:each)` is `true`, and `[{id: 1}].respond_to?(:each)` is `true`; - iterating a Hash yields key/value pairs, not rows, so treating a single Hash as a collection produces garbage. No capability distinguishes the two cases, so a class check is the clear, honest tool: ```ruby rows = [rows] if rows.is_a?(Hash) ``` A cleaner long-term fix is often two methods (`export_row`, `export_rows`), but when one entry point is required, the check is correct. ## Case 2: validating untrusted input Data parsed from JSON, YAML or form parameters has **no contract** with your code. A payload's `amount` may be `5`, `"5"`, `5.0` or `nil`. Checking at the boundary turns a vague failure later into a precise one now: 1. `raise TypeError, "amount must be an Integer, got #{amount.class}" unless amount.is_a?(Integer)` 2. After the check, the rest of the code can rely on Integer behaviour without defensive code everywhere. Here the class is the **specification**, not an implementation detail. ## Case 3: comparison and equality methods Methods such as `<=>` receive arbitrary objects. The Ruby documentation's own `Data` example guards `<=>` with `other.is_a?(self.class)` before reading `other.unit`, returning `nil` for anything else, which `Comparable` then turns into an `ArgumentError` (comparison failed) instead of a confusing `NoMethodError`. ## What class checks break | Object passed | Would duck typing work? | `is_a?(Array)` | |---|---|---| | `Set[[1, 2]]` | yes, it has `each` | `false`, rejected | | `rows.each_slice(100)` (an Enumerator) | yes | `false`, rejected | | `SimpleDelegator.new([[1, 2]])` | yes, `each` is forwarded | `false`, the delegator's own class is checked | | a subclass of Array | yes | `true` | The delegator row is the subtle one: `Delegator` forwards unknown methods to the wrapped object, but `is_a?` is a real method on the delegator itself, so it reports the wrapper's class. Decorators, test doubles and lazy proxies all fail class checks the same way. ## Making a necessary check less brittle - **Check the widest type that expresses the need.** `is_a?(Enumerable)` accepts Arrays, Sets, Hashes and Enumerators; `is_a?(Array)` accepts only Arrays and subclasses. - **Prefer `is_a?` to `instance_of?`**, so subclasses keep working. - **Check capability when that is the real requirement**: `respond_to?(:each)` accepts a bare class with only `each` and a delegator, both of which `is_a?(Enumerable)` rejects. - **Raise a useful error**: `TypeError` for the wrong kind of object, `ArgumentError` for the right kind with a bad value, with the actual class in the message. ## Where the check lives Put the check **once, at the edge** where the value enters: the controller action or job that parses the payload, or the public method of a library. Code behind that edge can then trust the type and stay duck-typed. Scattering `is_a?` checks through inner methods couples every layer to concrete classes and makes refactoring to a wrapper or a lazy collection painful. Gradual type signatures can document the same contract statically, but at runtime the boundary check is what stops a bad payload. ## A short decision list 1. Can the method just call what it needs? Then do that. 2. Does behaviour depend on an optional capability? Branch on `respond_to?`. 3. Do accepted shapes overlap, or is the input untrusted? Check `is_a?` against the widest meaningful class or module, at the boundary, once.
- Why does `SimpleDelegator.new([1]).is_a?(Array)` return false when the delegator forwards `each` and `size` to the Array?Delegator forwards only methods it does not define itself, through method_missing. `is_a?` comes from the copy of Kernel that Delegator includes, so it runs on the delegator and checks the delegator's own class, SimpleDelegator, which is not an Array.
- When would you raise TypeError and when ArgumentError after a failed check?TypeError when the object is the wrong kind altogether, such as a String where an Integer is required. ArgumentError when the kind is right but the value is not acceptable, such as a negative quantity. Including the actual class or value in the message makes the error actionable.
saying these in an interview costs you the question
- Duck typing means Ruby code should never check an object's class.
- respond_to?(:each) can tell a single Hash row from an Array of rows.
- A SimpleDelegator wrapping an Array passes is_a?(Array).
- Checking is_a?(Array) is the safest way to accept any collection.
- instance_of? is the better class check because it is stricter.