skip to content

In a Ruby case/when, what do Class#===, Range#===, Regexp#=== and Proc#=== each test, and how would one case use them together?

level: middleimportance: must knowfreq 58%

answer

  1. pattern classes override ===
  2. Module#=== is kind_of?
  3. Range#=== is cover, not iteration
  4. Regexp#=== false for nil or numbers
  5. Proc#=== calls the proc

basics

~20 s

Class#=== (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 s

Each 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 lines
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
  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)                   # => :ignore

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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.