rubymixinsruby-module

Identifying a Ruby module method invoked by super?


Ruby 2.7+.

I have methods in a couple of modules that are mixed in, and are invoked with super. Each of the module methods invokes super in turn, so all methods by that name in the mixed-in modules are invoked, although perhaps not in a deterministic order.

My question is: Can a method tell programmatically (as opposed to hard-coding) from what module it's been mixed in?

module A
  def initialize(*args, **kwargs)
    puts("something magic")
  end
end
module B
  def initialize(*args, **kwargs)
    puts("something magic")
  end
end

class C
  include A
  include B

  def initialize(*args, **kwargs)
    puts("actual #{class.name} initialize")
    super
  end
end

When run, this will print three lines. What I'm seeking is something like class.name, specific to each module, that identifies the module that supplied the initialize method that's running. The "something magic* strings would be replaced with this actual magic. 🙂

Thanks!


Solution

  • My first attempt was to call super_method to get all the initializers up the stack:

    module A
      def initialize
        A # return for comparison
      end
    end
    
    module B
      def initialize
        B
      end
    end
    
    class C
      include A
      include B
    
      def initialize
        C
      end
    
      def super_initializers
        init = method(:initialize)
        while init
          print init.call, " == "   # get hardcoded module from `initialize`
          p init.owner              # get the module dynamically
          init = init.super_method  # keep getting the super method
        end
      end
    end                     
    
    >> C.new.super_initializers
    C == C
    B == B
    A == A
     == BasicObject
    

    Second idea is to use Module.nesting, I think this is what you're looking for:

    module A
      def initialize
        # i, o, m = method(:initialize), [], Module.nesting[0]
        # while i; o << i.owner; i = i.super_method; end
        # print "prev "; p o[o.index(m)-1]  # previous super
        puts "A == #{Module.nesting[0]}"
        # print "next "; p o[o.index(m)+1]  # next super
        super
      end
    end
    
    module B
      def initialize
        puts "B == #{Module.nesting[0]}"
        super
      end
    end
    
    # add a class
    class Y
      def initialize
        puts "Y == #{Module.nesting[0]}"
        super
      end
    end
    
    # add some nesting
    module X
      class Z < Y
        def initialize
          puts "Z == #{Module.nesting[0]}"
          super
        end
      end
    end
    
    class C < X::Z
      include A
      include B
    
      def initialize
        puts "C == #{Module.nesting[0]}"
        super
      end
    end
    
    >> C.new
    C == C
    B == B
    A == A
    Z == X::Z
    Y == Y
    

    Actually, never thought about that this could be useful, but it works:

    def super_trace m
      while m;
        p m.owner; m = m.super_method
      end
    end
    
    >> super_trace User.new.method(:save)
    ActiveRecord::Suppressor
    ActiveRecord::Transactions
    ActiveRecord::Validations
    ActiveRecord::Persistence
    

    https://rubyapi.org/3.1/o/module#method-c-nesting

    https://rubyapi.org/3.1/o/method#method-i-super_method