In Ruby, how do you check whether a Product class mixes in Auditable, and what do Module#include? and Module#< report for include, prepend and extend?
answer
- include? walks the ancestors
- true for include and prepend
- extend lands on singleton_class
- argument must be a module
- later includes propagate since 3.0
basics
~20 sProduct.include?(Auditable) returns true when Auditable is included or prepended in Product or an ancestor. A module extended onto the class is not in its ancestors, so check Product.singleton_class.include?(M). Product < Auditable also returns true, or nil when unrelated.
solid answer
~40 s`Module#include?(mod)` returns `true` if `mod` is included **or prepended** in the receiver or any of its ancestors, so a subclass of `Product` also reports `true`. It requires a module: `Product.include?(BaseModel)` with a class raises `TypeError`, and `Auditable.include?(Auditable)` is `false`. `extend` does not touch the class's ancestors: after `extend AuditQueries`, `Product.include?(AuditQueries)` is `false` and `Product.singleton_class.include?(AuditQueries)` is `true`. `Module#<` treats mixins like superclasses: `Product < Auditable` is `true`, and it returns `nil` when there is no relationship. `included_modules` lists every mixed-in module. Since Ruby 3.0, including a module into `Auditable` after `Product` included it also shows up in `Product`'s ancestors, so `include?` reports it.
code
ruby · 15 linesmodule Auditable; end
module AuditQueries; end
class BaseModel; end
class Product < BaseModel
include Auditable
extend AuditQueries
end
Product.include?(Auditable) # => true
Product < Auditable # => true
Product.include?(AuditQueries) # => false
Product.singleton_class.include?(AuditQueries) # => true
Product < AuditQueries # => nil (no relationship)
Product.include?(BaseModel) # TypeError: wrong argument type Class (expected Module)go deeper
Know that Product.include?(Auditable) answers whether the class mixes the module in, and that subclasses report true too.
Explain why prepend counts but extend does not, where extend puts the module, and the TypeError and nil edge cases of include? and <.
Use membership checks in plugin code deliberately, prefer respond_to? in application code, and know that since Ruby 3.0 modules added to a mixin later reach existing includers.
Decide whether opt-in behaviour should be detected by module membership or by an explicit interface, since membership checks tie callers to the mixin structure rather than to capabilities.
## Module#include? `Module#include?(mod)` answers one question: does `mod` appear as a mixin in the receiver's ancestors? The core documentation spells out its rules: - `true` if the module is **included or prepended** in the receiver. - `true` if it is included or prepended in **any ancestor**, so subclasses inherit the answer. - `false` for the module itself: `Auditable.include?(Auditable)` is `false`. - The argument must be a **module**; passing a class raises `TypeError`. ```ruby class Product include Auditable prepend AuditSave extend AuditQueries end class Perishable < Product; end Product.include?(Auditable) # => true Product.include?(AuditSave) # => true (prepend counts) Perishable.include?(Auditable) # => true (inherited) Product.include?(AuditQueries) # => false (extend) Product.singleton_class.include?(AuditQueries) # => true ``` ## Where extend puts the module `extend` does not change the class's ancestors. It includes the module in the **singleton class** of the receiver, the hidden class that holds that one object's own methods. So membership checks for an extended module must ask that singleton class: | Mixed in with | `Product.include?(M)` | `Product.singleton_class.include?(M)` | |---|---|---| | `include M` | `true` | `false` | | `prepend M` | `true` | `false` | | `extend M` (on the class) | `false` | `true` | The same applies to a single extended instance: `movement.singleton_class.include?(ManualReview)`. ## Other membership checks 1. **`Module#<` and `Module#<=`** treat a mixed-in module like a superclass. `Product < Auditable` is `true`; `Product <= Product` is `true`; for unrelated modules the result is `nil`, not `false`, so test it with `if`, not with `== false`. 2. **`Module#included_modules`** returns every module included or prepended in the receiver or its ancestors, including `Kernel` for ordinary classes. 3. **`Module#ancestors.include?(M)`** works as well, and also reports classes, but builds an array on every call. For checking a single **instance** rather than a class, the type-check methods that also see modules are the usual tool, and in a `case` expression a module can be used as a `when` pattern. ## Later includes propagate (Ruby 3.0 and later) Until Ruby 2.7, including a module into `Auditable` after `Product` had already included `Auditable` did not reach `Product`: its ancestors had been computed at inclusion time. Since Ruby 3.0, `Module#include` and `Module#prepend` also affect classes and modules that had already included or prepended the receiver, as if the new module had been there from the start. ```ruby class Warehouse include Auditable end module Timestamps; end Auditable.include(Timestamps) Warehouse.include?(Timestamps) # => true on Ruby 3.0+, false on 2.7 ``` This matters for plugins and libraries that extend a shared module after models have loaded: on a current Ruby, every model sees the addition, and membership checks agree. ## A plugin example An inventory reporting plugin wants to list every auditable model. It can ask each class directly: ```ruby models = [Product, Warehouse, StockMovement] auditable = models.select { it.include?(Auditable) } queryable = models.select { it.singleton_class.include?(AuditQueries) } ``` The first check sees modules added with `include` or `prepend`, in the class or any superclass; the second sees modules added with `extend` on each class. Using `it`, the implicit block parameter available since Ruby 3.4, keeps the blocks short. If the plugin instead tested `respond_to?(:audit_trail)` on instances, it would work for any class that provides the method, whether through `Auditable` or its own code, which is often the more flexible contract. ## When to check membership at all - Checking `include?` is reasonable in framework or plugin code that needs to know whether a host opted in to a behaviour ("is this model auditable?"). - In application code, asking the object whether it `respond_to?` the method, or simply calling it, is usually simpler than checking which modules its class mixes in. - Prefer one check consistently: mixing `include?`, `<` and `ancestors.include?` in one codebase invites the `nil` versus `false` confusion.
- Why can Product < AuditQueries return nil instead of false?`Module#<` returns `nil` when neither module is an ancestor of the other. After `extend AuditQueries`, the module is in `Product`'s singleton class, not in `Product`'s ancestors, so there is no relationship to report. `nil` is falsy, so `if Product < AuditQueries` still works, but `== false` does not.
- How do you check whether a class is a subclass of BaseModel, given that include? rejects classes?Use `Product < BaseModel` or `Product <= BaseModel`, which work for superclasses and mixins alike. `Module#include?` is only for modules and raises `TypeError` when given a class.
saying these in an interview costs you the question
- Product.include?(M) is false when M was prepended rather than included
- Product.include?(M) is true after extend M inside the class body
- include? accepts a superclass and reports inheritance
- Module#< returns false for two unrelated modules
- A module included into Auditable later never reaches classes that already included Auditable