Feature #22514
openAdd an optional depth argument to Module#descendants
Description
Abstract¶
Let Module#descendants take an optional depth, and return only the
descendants that many steps from the receiver or fewer.
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 mostdepthsteps from
the receiver.descendantsanddescendants(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 raisesArgumentError.- 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:
Showing a large hierarchy a level or two at a time, as a documentation
tool, a REPL or a debugger might:
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#descendantsfor Ruby 4.1. - Ruby Facets'
Class#descendantshas 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.