How do you make your own Ruby class matchable by array and hash patterns, and what should deconstruct_keys do with its keys argument?
answer
- deconstruct for array and find
- deconstruct_keys for hash patterns
- keys: requested Symbols, or nil
- must return a Hash, else TypeError
- Const(...) checks with === first
basics
~20 sDefine deconstruct (returning an Array) for array and find patterns and deconstruct_keys(keys) (returning a Hash) for hash patterns. keys lists the Symbols the pattern needs, or is nil when it uses **rest, so you may compute only those keys.
solid answer
~40 sArray and find patterns call `deconstruct` on non-Array objects, and hash patterns call `deconstruct_keys(keys)` on non-Hash objects; an object without the method simply fails to match. `deconstruct` must return an Array and `deconstruct_keys` a Hash — anything else raises `TypeError` ("deconstruct_keys must return Hash"). The `keys` argument is an optimisation hint: an Array of the Symbol keys the pattern names, or `nil` when the pattern has `**rest` and therefore needs everything. Return at least those keys; extra keys are fine, and a missing key just makes the pattern fail. Prefixing the pattern with the class — `in WebhookEvent(type: "message", text:)` — first checks `WebhookEvent === value`, then deconstructs. Keep both methods pure: the reference leaves the number of calls undefined. Core examples: `Struct`, `Data`, `MatchData` and `Time` implement them.
code
ruby · 24 linesclass WebhookEvent
attr_reader :type, :team, :text
def initialize(type:, team:, text: nil)
@type, @team, @text = type, team, text
end
def deconstruct_keys(keys)
puts "deconstruct_keys(#{keys.inspect})"
{type:, team:, text:}
end
end
event = WebhookEvent.new(type: "message", team: "T1", text: "hi")
case event
in WebhookEvent(type: "message", text:) # prints deconstruct_keys([:type, :text])
p text # => "hi"
end
case event
in {type:, **rest} # prints deconstruct_keys(nil)
p rest # => {team: "T1", text: "hi"}
endgo deeper
Know that a class can take part in pattern matching by defining deconstruct for array patterns and deconstruct_keys for hash patterns.
Explain the keys argument, including nil for **rest, the required return types and the TypeError otherwise, and how Const(...) adds a === check.
Implement deconstruct_keys that computes only requested keys, stays pure because call counts are undefined, and exposes a stable Symbol-keyed view of a domain object.
Decide which domain objects expose a pattern-matching view, treating deconstruct_keys as public API whose keys become a compatibility promise to every caller that matches on them.
## The two hooks Pattern matching works on any object that opts in through two methods: | Pattern kind | Hook called | Must return | If missing | |---|---|---|---| | array pattern `[a, b]`, find pattern `[*, x, *]` | `deconstruct` | an `Array` | pattern does not match | | hash pattern `{type:, text:}` | `deconstruct_keys(keys)` | a `Hash` with Symbol keys | pattern does not match | `Array` and `Hash` implement these themselves (`Array#deconstruct`, `Hash#deconstruct_keys`), which is why literals just work. Returning the wrong type raises `TypeError` with the message `deconstruct must return Array` or `deconstruct_keys must return Hash`. ## A matchable webhook event ```ruby class WebhookEvent attr_reader :type, :team, :text, :raw def initialize(raw) @raw = raw @type = raw.fetch(:type) @team = raw[:team] @text = raw[:text] end def deconstruct_keys(keys) all = {type:, team:, text:} keys ? all.slice(*keys) : all.merge(raw: raw) end def deconstruct = [type, text] end ``` Now the router can match on the object directly: ```ruby case event in WebhookEvent(type: "message", text: String => text) reply(text) in WebhookEvent["reaction_added", _] :reaction end ``` ## What keys contains The reference explains that `keys` is passed "to provide a room for optimization in the matched class: if calculating a full hash representation is expensive, one may calculate only the necessary subhash". - For `in {type: "message", text:}`, `keys` is `[:type, :text]`. - When the pattern uses `**rest` (and therefore needs every key), `keys` is **`nil`**. Rules for a correct implementation: 1. **Return at least the requested keys** that exist. A key you omit makes the pattern fail as if the key were missing. 2. **Extra keys are harmless** for ordinary hash patterns, which ignore unrequested keys; they matter only with `**nil` or `**rest`. 3. **Handle `nil`** by returning the full representation. 4. **Use Symbol keys**, because hash patterns match only Symbols. `Struct#deconstruct_keys` shows the core behaviour: it accepts an Array or `nil` and raises `TypeError` for anything else. ## Class-qualified patterns `Const(...)` or `Const[...]` in front of a pattern adds a class check. The reference: "the expected class can be specified as part of the pattern and is checked with `===`". So `WebhookEvent(type: "message")`: 1. evaluates `WebhookEvent === event` (a kind-of check via `Module#===`); 2. calls `event.deconstruct_keys([:type])`; 3. matches the returned Hash against `{type: "message"}`. Parentheses and brackets are interchangeable; positional contents use `deconstruct`, keyword contents use `deconstruct_keys`. ## Purity and call counts The reference's "undefined behavior" appendix says the **number of `deconstruct` and `deconstruct_keys` calls is undefined**. CRuby caches a `deconstruct` result across the branches of one `case`, but you must not depend on either caching or repetition. Therefore: - no side effects (logging, counters, lazy loading that mutates state); - no expensive work beyond what `keys` asks for; - return values that do not change between calls. ## Core classes that already opt in - `Struct` and `Data`: both `deconstruct` and `deconstruct_keys`. - `MatchData`: `deconstruct` (the captures) and `deconstruct_keys` (named captures), since 3.2. - `Time`: `deconstruct_keys` with keys such as `:year` and `:hour`, since 3.2. So `in {year: 2026, month: 1..3}` works on a `Time` without any adapter. ## Summary Implement `deconstruct` for positional patterns and `deconstruct_keys(keys)` for hash patterns; return an Array and a Symbol-keyed Hash respectively; treat `keys` as a hint that may be `nil`; keep both pure. Prefix patterns with the class to add a `===` type check before deconstruction.
- What happens if deconstruct_keys ignores keys and always returns the full Hash?Matching still works: ordinary hash patterns ignore keys they did not request. You only lose the optimisation, which matters when computing some keys is expensive. Returning fewer keys than requested is the real bug, because the pattern then fails as if the key were missing.
- Why must deconstruct and deconstruct_keys be free of side effects?Ruby's reference leaves the number of calls undefined: CRuby may cache one result across branches, but another version or implementation may call again. Logging, counters or state changes inside them would therefore behave unpredictably. Keep them pure and cheap.
- How does `in WebhookEvent(type: "message")` differ from `in {type: "message"}` for the same object?The class-qualified form first checks `WebhookEvent === event`, so another object that happens to have a `deconstruct_keys` with a `:type` key will not match. The bare hash pattern accepts any object that deconstructs to a Hash with the right `:type` value.
deconstruct_keys is a records clerk handed a request slip: the slip lists the fields the pattern needs, so the clerk may copy only those pages. A blank slip (nil) means the caller wants the whole file, which is what a **rest pattern asks for.
saying these in an interview costs you the question
- Hash patterns call to_h on objects that do not define deconstruct_keys.
- deconstruct_keys must return exactly the requested keys and nothing more.
- keys is always an Array, even when the pattern uses **rest.
- Ruby guarantees deconstruct_keys is called once per case expression, so side effects are safe.
- Returning an Array from deconstruct_keys makes the pattern fail quietly.