Project

General

Profile

Actions

Feature #9779

closed

Add Module#descendants

Feature #9779: Add Module#descendants

Added by shugo (Shugo Maeda) over 12 years ago. Updated 8 days ago.

Status:
Closed
Assignee:
-
Target version:
-
[ruby-core:62193]

Description

Now classes have links to their subclasses, so how about to add Module#descendents?

class X
end

class Y < X
end

class Z < Y
end

p X.descendents    #=> [X, Y, Z]

module A
end

module B
  include A
end

module C
  include A
end

p A.descendents    #=> [A, C, B]

One of my own use cases is to implement AspectJ like pointcut which matches all subclasses of the given class.

Considerations:

  • Should the return value of Module#descendents include self?
  • Should the return value of Module#descendents include singleton classes?

I attach a patch, where the return value includes both self and singleton classes.


Files

0001-Add-Module-descendents.patch (3.75 KB) 0001-Add-Module-descendents.patch shugo (Shugo Maeda), 04/28/2014 07:00 AM

Related issues 1 (0 open1 closed)

Related to Ruby - Feature #14394: Class.descendantsClosedko1 (Koichi Sasada)Actions

Updated by shugo (Shugo Maeda) over 12 years ago Actions #1 [ruby-core:62195]

s/descendents/descendants/

Updated by naruse (Yui NARUSE) over 8 years ago Actions #2

  • Target version deleted (2.2.0)

Updated by jeremyevans0 (Jeremy Evans) almost 5 years ago Actions #3

Updated by shugo (Shugo Maeda) about 1 month ago Actions #4 [ruby-core:126047]

  • Subject changed from Add Module#descendents to Add Module#descendants

It seems that there's no progress since Class#descendants ([Feature #14394]) was reverted, so I've created a new pull request: https://github.com/ruby/ruby/pull/17807

I prefer Module#descendants because Class would automatically inherit Module#descendants but the converse does not hold.

I also believe Module#descendants is a more natural API than Class#descendants, because descendants should ideally be the dual of Module#ancestors; i.e., y ∈ x.descendants ⟺ x ∈ y.ancestors. Defining it at the Module level also answers the design question left open when [Feature #14394] was reverted (https://bugs.ruby-lang.org/issues/14394#note-33).

Assuming that Module#descendants is the dual of Module#ancestors, it should include self in its return value. However, I implemented Module#descendants not to include self, following ActiveSupport's Class#descendants and the reverted implementation of [Feature #14394]. Singleton classes are not included either, following the documented behavior of Class#subclasses. Note that [mod, *mod.descendants] can be used when self is needed.

The implementation reuses the existing subclass lists maintained for method cache invalidation, so no additional bookkeeping is introduced. Like Class#subclasses, the receiver does not hold strong references to its descendants, so the result may change after GC. Refinements are not included.

One use case of Module#descendants is Aspect Oriented Programming (illustrative, not a working example):

module Aspect
  def self.around(type, method_pattern, &advice)
    type.descendants.grep(Class).each do |klass|
      methods = klass.instance_methods(false).grep(method_pattern)
      next if methods.empty?

      wrapper = Module.new do
        methods.each do |name|
          define_method(name) do |*args, &blk|
            advice.call(self, name, args) { super(*args, &blk) }
          end
        end
      end
      klass.prepend(wrapper)
    end
  end
end

Aspect.around(Persistable, /\Asave/) do |obj, name, args, &proceed|
  logger.info("#{obj.class}##{name} start")
  proceed.call
ensure
  logger.info("#{obj.class}##{name} end")
end

A similar approach could also be used by instrumentation libraries such as OpenTelemetry and Datadog's dd-trace-rb.
Note that we have to use hooks like inherited or included to patch classes which will be loaded later.

Module#descendants is also useful for diagnostics, e.g., finding all classes a monkey patch is prepended to, or all models that include a specific Rails concern.

Updated by matz (Yukihiro Matsumoto) 9 days ago Actions #6 [ruby-core:126291]

Accepted. Let's define Module#descendants.

This settles the question I left open in #14394. Class#subclasses already lets you write the transitive closure for classes in Ruby, but there is no equivalent primitive for modules, so Module is where this method actually adds something. Defining it there also gives Class the method for free.

Not including self is fine too. It departs from ancestors, but it agrees with Class#subclasses and with ActiveSupport's descendants, and [mod, *mod.descendants] is available when needed.

Once this is merged, I think #14394 can be closed.

Matz.

Updated by shugo (Shugo Maeda) 8 days ago Actions #7

  • Status changed from Open to Closed

Applied in changeset git|96589334512bc960fabb5908ce8cc25a245ce090.


[Feature #9779] Add Module#descendants

Module#descendants returns an array of classes and modules that have
the receiver in their ancestors; it is the inverse of Module#ancestors.
The receiver itself, singleton classes, and refinements are not included.

Co-Authored-By: Claude Fable 5
Co-Authored-By: Claude Opus 5 (1M context)

Actions

Also available in: PDF Atom