Project

General

Profile

Actions

Feature #22514

open

Add an optional depth argument to Module#descendants

Feature #22514: Add an optional depth argument to Module#descendants

Added by trans (Thomas Sawyer) about 9 hours ago. Updated about 8 hours ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:127361]

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 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:

Serializable.descendants(1)

Showing a large hierarchy a level or two at a time, as a documentation
tool, a REPL or a debugger might:

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.
Actions

Also available in: PDF Atom