Feature #9779
closedAdd 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) about 1 month 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.
Updated by shugo (Shugo Maeda) 15 days ago
I've added specs to the pull request: https://github.com/ruby/ruby/pull/17807/changes/ed81db60a57ec586d4c5300282bc8c5ebcaa0e1e
Updated by matz (Yukihiro Matsumoto) 9 days ago
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
- 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 noreply@anthropic.com
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com