Look up all descendants of a class in Ruby
RubyRuby Problem Overview
I can easily ascend the class hierarchy in Ruby:
String.ancestors # [String, Enumerable, Comparable, Object, Kernel]
Enumerable.ancestors # [Enumerable]
Comparable.ancestors # [Comparable]
Object.ancestors # [Object, Kernel]
Kernel.ancestors # [Kernel]
Is there any way to descend the hierarchy as well? I'd like to do this
Animal.descendants # [Dog, Cat, Human, ...]
Dog.descendants # [Labrador, GreatDane, Airedale, ...]
Enumerable.descendants # [String, Array, ...]
but there doesn't seem to be a descendants
method.
(This question comes up because I want to find all the models in a Rails application that descend from a base class and list them; I have a controller that can work with any such model and I'd like to be able to add new models without having to modify the controller.)
Ruby Solutions
Solution 1 - Ruby
Here is an example:
class Parent
def self.descendants
ObjectSpace.each_object(Class).select { |klass| klass < self }
end
end
class Child < Parent
end
class GrandChild < Child
end
puts Parent.descendants
puts Child.descendants
puts Parent.descendants gives you:
GrandChild
Child
puts Child.descendants gives you:
GrandChild
Solution 2 - Ruby
If you use Rails >= 3, you have two options in place. Use .descendants
if you want more than one level depth of children classes, or use .subclasses
for the first level of child classes.
Example:
class Animal
end
class Mammal < Animal
end
class Dog < Mammal
end
class Fish < Animal
end
Animal.subclasses #=> [Mammal, Fish]
Animal.descendants #=> [Dog, Mammal, Fish]
Solution 3 - Ruby
Ruby 1.9 (or 1.8.7) with nifty chained iterators:
#!/usr/bin/env ruby1.9
class Class
def descendants
ObjectSpace.each_object(::Class).select {|klass| klass < self }
end
end
Ruby pre-1.8.7:
#!/usr/bin/env ruby
class Class
def descendants
result = []
ObjectSpace.each_object(::Class) {|klass| result << klass if klass < self }
result
end
end
Use it like so:
#!/usr/bin/env ruby
p Animal.descendants
Solution 4 - Ruby
Override the class method named inherited. This method would be passed the subclass when it is created which you can track.
Solution 5 - Ruby
Alternatively (updated for ruby 1.9+):
ObjectSpace.each_object(YourRootClass.singleton_class)
Ruby 1.8 compatible way:
ObjectSpace.each_object(class<<YourRootClass;self;end)
Note that this won't work for modules. Also, YourRootClass will be included in the answer. You can use Array#- or another way to remove it.
Solution 6 - Ruby
Although using ObjectSpace works, the inherited class method seems to be better suitable here inherited(subclass) Ruby documentation
Objectspace is essentially a way to access anything and everything that's currently using allocated memory, so iterating over every single one of its elements to check if it is a sublass of the Animal class isn't ideal.
In the code below, the inherited Animal class method implements a callback that will add any newly created subclass to its descendants array.
class Animal
def self.inherited(subclass)
@descendants = []
@descendants << subclass
end
def self.descendants
puts @descendants
end
end
Solution 7 - Ruby
I know you are asking how to do this in inheritance but you can achieve this with directly in Ruby by name-spacing the class (Class
or Module
)
module DarthVader
module DarkForce
end
BlowUpDeathStar = Class.new(StandardError)
class Luck
end
class Lea
end
end
DarthVader.constants # => [:DarkForce, :BlowUpDeathStar, :Luck, :Lea]
DarthVader
.constants
.map { |class_symbol| DarthVader.const_get(class_symbol) }
.select { |c| !c.ancestors.include?(StandardError) && c.class != Module }
# => [DarthVader::Luck, DarthVader::Lea]
It's much faster this way than comparing to every class in ObjectSpace
like other solutions propose.
If you seriously need this in a inheritance you can do something like this:
class DarthVader
def self.descendants
DarthVader
.constants
.map { |class_symbol| DarthVader.const_get(class_symbol) }
end
class Luck < DarthVader
# ...
end
class Lea < DarthVader
# ...
end
def force
'May the Force be with you'
end
end
benchmarks here: http://www.eq8.eu/blogs/13-ruby-ancestors-descendants-and-other-annoying-relatives
update
in the end all you have to do is this
class DarthVader
def self.inherited(klass)
@descendants ||= []
@descendants << klass
end
def self.descendants
@descendants || []
end
end
class Foo < DarthVader
end
DarthVader.descendants #=> [Foo]
thank you @saturnflyer for suggestion
Solution 8 - Ruby
(Rails <= 3.0 ) Alternatively you could use ActiveSupport::DescendantsTracker to do the deed. From source:
> This module provides an internal implementation to track descendants which is faster than iterating through ObjectSpace.
Since it is modularize nicely, you could just 'cherry-pick' that particular module for your Ruby app.
Solution 9 - Ruby
A simple version that give an array of all the descendants of a class:
def descendants(klass)
all_classes = klass.subclasses
(all_classes + all_classes.map { |c| descendants(c) }.reject(&:empty?)).flatten
end
Solution 10 - Ruby
Ruby Facets has Class#descendants,
require 'facets/class/descendants'
It also supports a generational distance parameter.
Solution 11 - Ruby
Rails provides a subclasses method for every object, but it's not well documented, and I don't know where it's defined. It returns an array of class names as strings.
Solution 12 - Ruby
You can require 'active_support/core_ext'
and use the descendants
method. Check out the doc, and give it a shot in IRB or pry. Can be used without Rails.
Solution 13 - Ruby
Building on other answers (particularly those recommending subclasses
and descendants
), you may find that in Rails.env.development, things get confusing. This is due to eager loading turned off (by default) in development.
If you're fooling around in rails console
, you can just name the class, and it will be loaded. From then on out, it will show up in subclasses
.
In some situations, you may need to force the loading of classes in code. This is particularly true of Single Table Inheritance (STI), where your code rarely mentions the subclasses directly. I've run into one or two situations where I had to iterate all the STI subclasses ... which does not work very well in development.
Here's my hack to load just those classes, just for development:
if Rails.env.development?
## These are required for STI and subclasses() to eager load in development:
require_dependency Rails.root.join('app', 'models', 'color', 'green.rb')
require_dependency Rails.root.join('app', 'models', 'color', 'blue.rb')
require_dependency Rails.root.join('app', 'models', 'color', 'yellow.rb')
end
After that, subclasses work as expected:
> Color.subclasses
=> [Color::Green, Color::Blue, Color::Yellow]
Note that this is not required in production, as all classes are eager loaded up front.
And yes, there's all kinds of code smell here. Take it or leave it...it allows you to leave eager loading off in development, while still exercising dynamic class manipulation. Once in prod, this has no performance impact.
Solution 14 - Ruby
Using descendants_tracker gem may help. The following example is copied from the gem's doc:
class Foo
extend DescendantsTracker
end
class Bar < Foo
end
Foo.descendants # => [Bar]
This gem is used by the popular virtus gem, so I think it's pretty solid.
Solution 15 - Ruby
This method will return a multidimensional hash of all of an Object's descendants.
def descendants_mapper(klass)
klass.subclasses.reduce({}){ |memo, subclass|
memo[subclass] = descendants_mapper(subclass); memo
}
end
{ MasterClass => descendants_mapper(MasterClass) }
Solution 16 - Ruby
To compute the transitive hull of an arbitrary class
def descendants(parent: Object)
outSet = []
lastLength = 0
outSet = ObjectSpace.each_object(Class).select { |child| child < parent }
return if outSet.empty?
while outSet.length == last_length
temp = []
last_length = outSet.length()
outSet.each do |parent|
temp = ObjectSpace.each_object(Class).select { |child| child < parent }
end
outSet.concat temp
outSet.uniq
temp = nil
end
outSet
end
end
Solution 17 - Ruby
Class#descendants (Ruby 3.1+)
Starting from Ruby 3.1, Class#descendants is a build-in method.
It returns all descendants of a class excluding the receiver and singleton classes.
As a result, there is no more need to depend on ActiveSupport or write monkey-patches in order to use it.
class A; end
class B < A; end
class C < B; end
A.descendants #=> [B, C]
B.descendants #=> [C]
C.descendants #=> []
Sources:
- Class#descendants from official Ruby docs.
- Add Class#descendants.
- Feature #14394.
- Ruby 3.1 Class#descendants.
Solution 18 - Ruby
If you have access to code before any subclass is loaded then you can use inherited method.
If you don't (which is not a case but it might be useful for anyone who found this post) you can just write:
x = {}
ObjectSpace.each_object(Class) do |klass|
x[klass.superclass] ||= []
x[klass.superclass].push klass
end
x[String]
Sorry if I missed the syntax but idea should be clear (I don't have access to ruby at this moment).