Feature #9779
openAdd Module#descendants
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
Updated by shugo (Shugo Maeda) over 12 years ago
s/descendents/descendants/
Updated by naruse (Yui NARUSE) over 8 years ago
- Target version deleted (
2.2.0)
Updated by jeremyevans0 (Jeremy Evans) almost 5 years ago
- Related to Feature #14394: Class.descendants added
Updated by shugo (Shugo Maeda) 20 days ago
- 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.