Look up all descendants of a class in Ruby
Asked Answered
M

19

161

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

Mackle answered 6/3, 2010 at 18:47 Comment(0)
W
168

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
Walford answered 6/3, 2010 at 19:4 Comment(6)
That works great, thanks! I suppose visiting every class might be too slow if you're trying to shave milliseconds, but it's perfectly speedy for me.Mackle
singleton_class instead of Class make it much faster (see source at apidock.com/rails/Class/descendants)Krahmer
Be careful if you might have a situation where the class is not loaded in the memory yet, ObjectSpace won't have it.Uella
How could I make this work for Object and BasicObject ?, curious to know what they show upWinifred
@AmolPujari p ObjectSpace.each_object(Class) will print out all the classes. You can also get the descendants of any class you want by substutiting its name for self in the line of code in the method.Veranda
Is there a way to modify this approach to generate the transitive hull of all klass < self but instead of directly using self as second argument using a range of classes between klass and self building a chain like klass < A < B < C < D < self ? That is, < would return the transitive hull of "inherited from."Brittne
C
77

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]
Cinnabar answered 4/10, 2013 at 17:59 Comment(3)
Note that in development, if you have eager loading turned off, these methods will only return classes if they have been loaded (i.e. if they've been referenced already by the running server).Prudi
@Prudi what's the safest way of guaranteeing correct results here?Embarkation
In development mode, use require_dependency to autoload any necessary files BEFORE .subclasses, .descendants is called. To get the needed paths, use a path or a glob, such as Rails.root.glob('pattern/to/*/file_*.rb). It can even be done in the initializer as explained in an answer here: #29663018Coverture
Q
26

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
Quinones answered 6/3, 2010 at 19:48 Comment(2)
This works for Modules too; just replace both instances of "Class" with "Module" in the code.Dozy
For extra safety one should write ObjectSpace.each_object(::Class) - this will keep the code working when you happen to have a YourModule::Class defined.Hortative
C
23

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

Cragsman answered 6/3, 2010 at 18:52 Comment(5)
I like this one too. Overriding the method is marginally intrusive, but it makes the descendant method a little more efficient since you don't have to visit every class.Mackle
@Douglas While it is less intrusive, you will probably have to experiment to see if it meets your needs (i.e. when does Rails build the controller/model hierarchy?).Metrify
It's also more portable to various non-MRI ruby implementations, some of which have serious performance overhead from use of ObjectSpace. Class#inherited is ideal for implementing "auto-registration" patterns in Ruby.Exegesis
Care to share an example? Since it's class level I guess you would have to store each class in some sort of global variable?Isooctane
@Isooctane No, an instance variable on the class itself. But then the objects cannot be collected by GC.Churinga
P
15

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.

Piassava answered 21/5, 2011 at 22:45 Comment(2)
that was awesome. Can you explain to me how that works? I used ObjectSpace.each_object(class<<MyClass;self;end) {|it| puts it}Roberson
In ruby 1.8, class<<some_obj;self;end returns the singleton_class of an object. In 1.9+ you can use some_obj.singleton_class instead (updated my answer to reflect that). Every object is an instance of its singleton_class, which applies for classes too. Since each_object(SomeClass) returns all instances of SomeClass, and SomeClass is an instance of SomeClass.singleton_class, each_object(SomeClass.singleton_class) will return SomeClass and all subclasses.Piassava
V
10

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
Vertebra answered 6/2, 2016 at 13:50 Comment(2)
` @descendants ||= []` otherwise you'll only get the last descendantKaffiyeh
Because of the name @descendants this example is misleading: you'll get only the (immediate) subclasses. But recursively: any subclasses of Animal will also inherit inherited, and it will fire for its own subclasses. To get really descendants, use @@descendants.Irresolute
W
5

Class#subclasses (Ruby 3.1+)

Starting from Ruby 3.1, there is a built-in method - Class#subclasses.

It returns an array of classes where the receiver is the direct superclass of the class, excluding 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
class D < A; end

A.subclasses        #=> [D, B]
B.subclasses        #=> [C]
C.subclasses        #=> []

Sources:

Wain answered 24/11, 2021 at 23:36 Comment(1)
Class#subclasses exists in Ruby 3.1. Only lists immediate children of Class. Does not include singleton classes.Sternmost
I
4

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

Institutionalism answered 24/4, 2015 at 6:17 Comment(0)
J
3

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

Jack answered 6/8, 2011 at 2:23 Comment(0)
C
3

Ruby Facets has Class#descendants,

require 'facets/class/descendants'

It also supports a generational distance parameter.

Chauvin answered 21/10, 2012 at 12:42 Comment(0)
E
3

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
Exsect answered 23/12, 2016 at 19:50 Comment(3)
This looks like a superior answer. Unfortunately it still falls prey to lazy-loading of classes. But I think they all do.Forwhy
@DaveMorse I ended up listing files and manually loading the constants to have them registered as descendants (and then ended up removing this whole thing :D)Exsect
Note that #subclasses is from Rails ActiveSupport.Accusative
A
1

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.

Arteritis answered 16/8, 2011 at 19:38 Comment(0)
P
1

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.

Prebend answered 7/2, 2014 at 18:23 Comment(2)
If you have to add active support to your Gemfile, then it's not really "without rails". It's just choosing the pieces of rails you like.Ermine
This seems like a philosophical tangent that is not relevant to the topic here, but I think that using a Rails component does necessarily mean one is using Rails in a holistic sense.Prebend
C
1

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.

Cully answered 15/10, 2020 at 20:24 Comment(0)
S
0

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.

Stationmaster answered 24/10, 2015 at 13:47 Comment(0)
P
0

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) }
Pinsky answered 4/5, 2016 at 16:59 Comment(0)
B
0

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
Brittne answered 20/10, 2020 at 11:1 Comment(0)
S
0

For Ruby 3.1+ Class#subclasses is available. Class#descendants is not implemented:

class A; end
class B < A; end
class C < B; end
class D < A; end

A.subclasses => [B, D]

A.descendants => NoMethodError: undefined method 'descendants' for A:Class

A.methods.grep('descendants') => []

For Ruby < 3.1 this is slightly faster than the Rails implementation:

def descendants
  ObjectSpace.each_object(singleton_class).reduce([]) do |des, k|
    des.unshift k unless k.singleton_class? || k == self
    des
  end
end

The Ruby 3.1+ #subclasses appears much faster than the descendants method given above.

Sternmost answered 25/9, 2022 at 5:10 Comment(0)
H
-1

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

Holpen answered 6/3, 2010 at 18:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.