Look up all descendants of a class in Ruby

Ruby

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

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionDouglas SquirrelView Question on Stackoverflow
Solution 1 - RubyPetrosView Answer on Stackoverflow
Solution 2 - RubydgilperezView Answer on Stackoverflow
Solution 3 - RubyJörg W MittagView Answer on Stackoverflow
Solution 4 - RubyChandra SekarView Answer on Stackoverflow
Solution 5 - RubyapeirosView Answer on Stackoverflow
Solution 6 - RubyMateo SossahView Answer on Stackoverflow
Solution 7 - Rubyequivalent8View Answer on Stackoverflow
Solution 8 - RubychtrinhView Answer on Stackoverflow
Solution 9 - RubyDorianView Answer on Stackoverflow
Solution 10 - RubytransView Answer on Stackoverflow
Solution 11 - RubyDaniel TsadokView Answer on Stackoverflow
Solution 12 - RubymyconodeView Answer on Stackoverflow
Solution 13 - RubyDavid HempyView Answer on Stackoverflow
Solution 14 - RubyEricCView Answer on Stackoverflow
Solution 15 - RubyjozwrightView Answer on Stackoverflow
Solution 16 - Rubyvon spotzView Answer on Stackoverflow
Solution 17 - RubyMarian13View Answer on Stackoverflow
Solution 18 - RubyMaciej PiechotkaView Answer on Stackoverflow