153
votes

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

17

17 Answers

157
votes

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
69
votes

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]
26
votes

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
22
votes

Override the class method named inherited. This method would be passed the subclass when it is created which you can track.

14
votes

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.

10
votes

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
4
votes

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

3
votes

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

3
votes

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
2
votes

Ruby Facets has Class#descendants,

require 'facets/class/descendants'

It also supports a generational distance parameter.

1
votes

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.

1
votes

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.

1
votes

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.

0
votes

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.

0
votes

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) }
0
votes

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
-1
votes

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