In a Ruby case/when, what do Class#===, Range#===, Regexp#=== and Proc#=== each test, and how would one case use them together?
answer
- pattern classes override ===
- Module#=== is kind_of?
- Range#=== is cover, not iteration
- Regexp#=== false for nil or numbers
- Proc#=== calls the proc
basics
~20 sClass#=== (from Module) tests instance-of-or-subclass, Range#=== tests whether the value lies between begin and end, Regexp#=== tests for a match in a string, and Proc#=== calls the proc. One case can mix them to classify input by type, range, pattern and predicate.
solid answer
~50 sEach pattern class overrides `===`, which `case/when` calls with the pattern as receiver. `Class#===` is inherited from `Module#===` and returns true when the subject is an instance of the class or of a descendant, so `when Integer` matches `503`. `Range#===` returns true when the subject lies between the range's begin and end — it checks the bounds, it does not enumerate — and it returns `false` for incomparable values such as a String against an Integer range. `Regexp#===` returns true when the regexp finds a match in a String (a Symbol, or an object with `to_str`, is converted); `nil`, numbers and other objects return `false` instead of raising. `Proc#===` calls the proc with the subject and uses its result, which lets a lambda act as a predicate. A monitoring bot can therefore route `case input` through `when Integer`, `when 500..599`, `when /timeout/i` and `when ->(x) { x.respond_to?(:status) }` in one expression.
code
ruby · 17 linesdef route(input)
case input
when 200..299 then :ok
when 500..599 then :page
when Integer then :log
when /timeout|refused/i then :network
when String, Symbol then :note
else :ignore
end
end
p route(204) # => :ok
p route(503) # => :page
p route(404) # => :log
p route("Connection refused") # => :network
p route(:heartbeat) # => :note
p route(nil) # => :ignorego deeper
Name what each pattern matches: a class matches its instances, a range matches values between its ends, a regexp matches strings it finds a match in, and a lambda matches when it returns truthy.
Explain Module#=== as a kind-of test, Range#=== as a bounds check rather than iteration, and why Regexp#=== and Range#=== return false instead of raising for the wrong type.
Order clauses from specific to general, keep expensive procs last, and make custom predicates total so unexpected input falls through instead of raising in a production dispatcher.
Judge when a growing case over types should become polymorphism or a registry of matchers, and how to keep classification rules reviewable and testable as they multiply.
## One operator, many meanings `case/when` never asks "are these equal?". It calls **`when_value === subject`** and lets the class of the `when` value decide what matching means. Ruby's core classes override `===` so that different kinds of object act as different kinds of **pattern**. | Pattern | Defined as | True when | Returns for a mismatched type | |---|---|---|---| | `Integer`, `String`, any class or module | `Module#===` | subject is an instance of it or of a descendant | `false` | | `500..599`, `"a".."m"` | `Range#===` | subject lies between begin and end | `false` (e.g. `(1..4) === "a"`) | | `/timeout/i` | `Regexp#===` | the regexp finds a match in the subject | `false` for `nil`, numbers and other non-strings | | `->(x) { ... }` | `Proc#===` | the proc's result is truthy | whatever the proc does | | `200`, `"GET"`, `:get` | `Integer#===`, `String#===`, `Symbol#===` | plain equality | `false` | ## Class and module patterns The Ruby documentation for `Module#===` says it returns true "if obj is an instance of mod or an instance of one of mod's descendants" — the same test as `kind_of?`. Since every class is a module, `when Integer`, `when Numeric` and `when Comparable` all work: - `Integer === 503` is `true`; - `Numeric === 503` is `true` (Integer descends from Numeric); - `Comparable === "x"` is `true` (String includes Comparable). ## Range patterns `Range#===` checks that the subject is **between** `begin` and `end`, honouring `...` exclusion; in CRuby it is implemented with the same bounds check as `cover?`. It does not walk the elements, so `("a".."d") === "cc"` is `true` even though `"cc"` is not one of the range's elements. Endless and beginless ranges work as open bounds: ```ruby case latency_ms when ...100 then :fast when 100..999 then :slow when 1000.. then :timeout_risk end ``` A value that cannot be compared with the bounds simply returns `false`, so `(1..4) === "a"` does not raise. ## Regexp patterns `Regexp#===` returns `true` or `false` — not a match position — so it is safe as a `when` value. The subject may be a String; a Symbol, or an object that converts implicitly with `to_str`, is turned into a string; any other object (including `nil` and numbers) makes it return `false` rather than raise. As a side effect a successful match also sets the last-match data, which regexp-specific code can read afterwards. ## Proc patterns `Proc#===` is documented as invoking the block "with obj as the proc's parameter like Proc#call. This allows a proc object to be the target of a when clause". The result is used for its truthiness: ```ruby SERVER_ERROR = ->(code) { code.is_a?(Integer) && code >= 500 } case status when SERVER_ERROR then page_on_call end ``` `Method#===` exists too, so `when method(:retryable?)` works the same way. ## Putting them together: a monitoring bot ```ruby def route(input) case input when 200..299 then :ok when 500..599 then :page when Integer then :log when /timeout|refused/i then :network when String, Symbol then :note when ->(x) { x.respond_to?(:status) } then route(input.status) else :ignore end end ``` Things to notice: 1. **Order matters.** `when Integer` sits after the ranges; placed first, it would swallow every status code. 2. **Mismatched types are harmless.** A String reaching `when 200..299` makes `Range#===` return `false`, and an Integer reaching the regexp makes `Regexp#===` return `false`. 3. **The lambda is last** because it is the most general and the most expensive test. 4. `Integer === 200.0` is `false`: `200.0` is a Float, so a Float status falls through the class branch — but `Range#===` compares numerically, so `(200..299) === 200.0` is `true`. ## Summary `Module#===` tests type, `Range#===` tests bounds, `Regexp#===` tests for a match in a string, `Proc#===` runs a predicate. Because each returns `false` rather than raising for the wrong kind of subject, one `case` can mix them freely — as long as you order the `when` clauses from most to least specific.
- Why does `("a".."d") === "cc"` return true?`Range#===` checks bounds, the same way `cover?` does: `"cc"` sorts after `"a"` and before `"d"`, so it is between them. It does not enumerate the range's elements, which would give `a`, `b`, `c`, `d` and exclude `"cc"`. This is what makes ranges usable as `when` values for unbounded types like strings and floats.
- What does `/timeout/ === nil` return, and why does that matter in a case?It returns `false`. `Regexp#===` converts a Symbol (or an object with `to_str`) to a String, and for `nil`, numbers and other objects it returns `false` rather than raising. That means a `when /timeout/` clause is safe even when the subject is `nil` or an Integer; the case simply moves on to the next `when`.
- Why must a broad class pattern like `when Integer` come after narrower ranges?Because `case` stops at the first truthy `===`. `Integer === 503` is true, so a `when Integer` clause placed before `when 500..599` catches every status code and the range branch becomes unreachable. Order `when` clauses from most specific to most general.
saying these in an interview costs you the question
- when Integer compares the subject's class with Integer using ==.
- Range#=== iterates the range and checks each element, so it is slow for large ranges.
- Regexp#=== raises TypeError when the subject is nil.
- A lambda cannot be used as a when value; you need an if for predicates.
- (1..4) === "a" raises ArgumentError because the types differ.