From: "trans (Thomas Sawyer) via ruby-core" Date: 2026-11-08T21:12:00+00:00 Subject: [ruby-core:127362] [Ruby Feature#22514] Add an optional depth argument to Module#descendants Issue #22514 has been updated by trans (Thomas Sawyer). Pull request with the patch, tests and specs: https://github.com/ruby/ruby/pull/19777 ---------------------------------------- Feature #22514: Add an optional depth argument to Module#descendants https://bugs.ruby-lang.org/issues/22514#change-119793 * Author: trans (Thomas Sawyer) * Status: Open ---------------------------------------- # Abstract Let `Module#descendants` take an optional depth, and return only the descendants that many steps from the receiver or fewer. ```ruby module A; end module B; include A; end class C; include B; end class D < C; end A.descendants #=> [B, C, D] A.descendants(1) #=> [B, C] C.descendants(1) #=> [D] ``` # Background `Module#descendants` (#9779) returns every class and module that has the receiver among its ancestors. Often only the nearest ones are wanted. For a class, the nearest ones are `Class#subclasses`, and any depth can be built from it, as Matz noted in #9779. For a module there is no such primitive. Which classes and modules mix in `M` themselves, rather than inheriting it from a superclass? Today that takes filtering `M.descendants` by hand, and the obvious filter, rejecting classes whose superclass already includes `M`, gets it wrong for a class that prepends `M` when its superclass includes it. # Proposal * `descendants(depth)` returns the descendants at most `depth` steps from the receiver. `descendants` and `descendants(nil)` return all of them, as now. * The first step reaches the receiver's subclasses, and the classes and modules that include or prepend it, whether directly or through another module they include. Each further step reaches the subclasses of the descendants already found, and their includers. * For a class, `descendants(1)` returns the same classes as `Class#subclasses`. * When a descendant can be reached by more than one path, the shortest counts. * `descendants(0)` returns `[]`. A negative depth raises `ArgumentError`. * The order of the result is still not defined. # Use cases Finding the classes that mix in a module themselves, such as the classes that include a plugin or serialization module, as opposed to their subclasses: ```ruby Serializable.descendants(1) ``` Showing a large hierarchy a level or two at a time, as a documentation tool, a REPL or a debugger might: ```ruby StandardError.descendants(2) ``` Avoiding a walk of the whole hierarchy when only the top of it is needed, as with `ActiveRecord::Base` in a large application. # Discussion The main question is what counts as a step for modules. In the example above, `C` includes `B`, which includes `A`. This proposal counts `C` as one step from `A`, like `B`. That matches `C.ancestors`, where `A` sits in `C`'s own part of the chain, ahead of its superclass, and it's the shape Ruby's own traversal already walks, so the depth falls out of it. Counting `C` as two steps from `A` would need Ruby to track which module each inclusion came through, which it doesn't today. Naming: `depth` reads naturally next to `ancestors` and `descendants`. Ruby Facets calls the same argument `generations`. Compatibility: the change only adds an optional argument. The C function `rb_mod_descendants` keeps its signature. # See also * #9779, which added `Module#descendants` for Ruby 4.1. * Ruby Facets' `Class#descendants` has taken a generation limit since 2010, for classes only. * A patch, with tests and specs, is ready, and I'll link the pull request here. -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/