skip to content

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?

level: middleimportance: should knowfreq 30%

answer

  1. include? walks the ancestors
  2. true for include and prepend
  3. extend lands on singleton_class
  4. argument must be a module
  5. later includes propagate since 3.0

basics

~20 s

Product.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 lines
ruby
module 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

for a junior

Know that Product.include?(Auditable) answers whether the class mixes the module in, and that subclasses report true too.

for a middle

Explain why prepend counts but extend does not, where extend puts the module, and the TypeError and nil edge cases of include? and <.

for a senior

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.

for a principal

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