Search code examples
rubyinheritancereflectionmetaprogrammingcrystal-lang

Getting info about inheritance chain in Crystal


Just out of curiosity and to learn a little bit about the general structure of Crystal, I was looking for some reflection features that would allow me to better understand how the inheritance chain is built.

I was thinking something like ruby's superclass, ancestors or included_modules methods.

Is there something like that available in Crystal language?

Moreover, it would be useful to have some kind of diagram that could show me the big picture.


Solution

  • Metaprogramming in Crystal is achieved using macros.

    Macros are methods that receive AST nodes at compile-time and produce code that is pasted into a program.

    Crystal already has implementations of superclass and ancestors which return results at compile time. So you can do:

    {{ pp MyClass.superclass }}
    {{ pp MyClass.ancestors }}
    

    For convenience, to inspect the inheritance you can write your own macros. Having learning purpose in mind, it may look like this:

    class Class
      def superclass
        {{ @type.superclass }}
      end
    
      def ancestors
        {% if @type.ancestors.size > 0 %}
          {{ @type.ancestors }}
        {% else %}
          [] of Nil
        {% end %}
      end
    
      def included_modules
        {% if @type.ancestors.any? { |a| a.class.stringify.ends_with?(":Module") } %}
          {{ @type.ancestors.select { |a| a.class.stringify.ends_with?(":Module") } }}
        {% else %}
          [] of Nil
        {% end %}
      end
    
      def inheritance_chain
        String.build do |chain|
          cls = self
          chain << cls
          while !(cls = cls.superclass).nil?
            chain << " > #{cls}"
          end
        end
      end
    end
    

    Then you can do the inspection:

    class A
    end
    
    module B
    end
    
    require "crystal/system/random"
    
    class C < A
      include B
      include Crystal::System::Random
    end
    
    C.name             # => "C"
    C.superclass       # => A
    C.ancestors        # => [Crystal::System::Random, B, A, Reference, Object]
    C.included_modules # => [Crystal::System::Random, B]
    A.included_modules # => []
    

    And if you go further:

    C.superclass                                                    # => A
    C.superclass.try &.superclass                                   # => Reference
    C.superclass.try &.superclass.try &.superclass                  # => Object
    C.superclass.try &.superclass.try &.superclass.try &.superclass # => nil
    

    And now using inheritance_chain:

    Int32.inheritance_chain                          # => "Int32 > Int > Number > Value > Object"
    String.inheritance_chain                         # => "String > Reference > Object"
    Float64.inheritance_chain                        # => "Float64 > Float > Number > Value > Object"
    Array(Bool).inheritance_chain                    # => "Array(Bool) > Reference > Object"
    Hash(Bool, Bool).inheritance_chain               # => "Hash(Bool, Bool) > Reference > Object"
    Tuple(Char).inheritance_chain                    # => "Tuple(Char) > Struct > Value > Object"
    NamedTuple(s: String, b: Bool).inheritance_chain # => "NamedTuple(s: String, b: Bool) > Struct > Value > Object"
    Nil.inheritance_chain                            # => "Nil > Value > Object"
    Regex.inheritance_chain                          # => "Regex > Reference > Object"
    Symbol.inheritance_chain                         # => "Symbol > Value > Object"
    Proc(Int32).inheritance_chain                    # => "Proc(Int32) > Struct > Value > Object"
    Set(String).inheritance_chain                    # => "Set(String) > Struct > Value > Object"
    Exception.inheritance_chain                      # => "Exception > Reference > Object"
    Class.inheritance_chain                          # => "Class > Value > Object"
    
    # union
    alias UnionType = Int32 | Nil | String
    UnionType.inheritance_chain                      # => "(Int32 | String | Nil) > Value > Object"
    
    # nilable
    Int32?.inheritance_chain                         # => "(Int32 | Nil) > Value > Object"
    
    # pointer
    alias Int32Ptr = Int32*
    Int32Ptr.inheritance_chain                       # => "Pointer(Int32) > Struct > Value > Object"
    
    # ...