Search code examples
rubymixins

method that is both callable as class method and mixed in as a class method?


In Ruby, is it possible to define a method that can be called directly as a class method and is also able to be mixed in as a class method? That is, without using self.included or self.extended to create an equivalent class or instance method.

Neither of these approaches works:

module A
  def foo(s)
    puts s
  end
end

class One
  extend A
end

One.foo("one")
#A.foo("a")

module B
  def self.foo(s)
    puts s
  end
end

class Two
  include B
end

#Two.foo("two")
B.foo("b")

There seems to be some confusion as to what is being asked. Here's a less abstract scenario. A is a mixin that can be used directly. B is a mixin, intended to be used independently of A, that "wraps" one of A's methods.

module A
  # #foo has to be defined in order to be mixed in via `extend`.
  # Being mixed in via `include` has the same issue but inverted.
  def foo(s) A.foo(s) end 
  def self.foo(s) puts "A: " + s end
end

module B
  def foo(s) A.foo("B: " + s) end
end

class One; extend A end

class Two; extend B end

One.foo("one")
Two.foo("two")

In order for this to work, both A#foo and A::foo have to be separately defined. Existing facilities such as Module#module_function don't work under this scenario.


Solution

  • I will try not to be definitive but to my knowledge the answer to your question is No. If you would like to mixin both instance and class methods then the standard methodology would be something like:

    module A 
      def self.included(base)
        #this will extend the class you included A in
        #using A::ClassMethods definition
        base.extend(ClassMethods)
      end
      #these methods will be added as class_methods to any class
      #that includes A
      module ClassMethods
        def foo(s)
          "You fooed the class with #{s}"
        end
      end
      #this will be added as an instance method as it would be in a standard include
      def bar(s)
        "You barred an instance with #{s}"
      end
    end
    
    class Mixed
      include A
    end
    Mixed.foo("Hello")
    #=> "You fooed the class with Hello"
    Mixed.new.bar("Hello")
    #=> "You barred an instance with Hello"
    

    I hope this answers your question as it was a bit unclear what your intentions were. Since you question does not seem to require instance methods you could also do this

    module A 
      def foo(s)
        "called foo with #{s}"
      end
    end
    module B
      include A
      alias_method :a_foo, :foo
      def foo(s)
        "B called foo from A #{a_foo(s)}"
      end
    end
    class Mixed
      extend B
    end
    
    Mixed.foo("Mixed")
    #=>"B called foo from A called foo with Mixed"
    

    One more update

    This is a strange pattern but it will work for your use case I believe

    module A
      def foo(s)
        "fooed with #{s}"
      end
      def bar(s)
        "barred with #{s}"
      end
    end
    
    module B
      include A
      included_modules.each do |mod|
        (mod.instance_methods - Object.methods).each do |meth|
          alias_method "#{mod.name.downcase}_#{meth}", meth
        end
      end
    end
    
    class Mixed
      extend B
    end
    
    Mixed.methods - Object.methods 
    #=> [:a_foo, :a_bar, :foo, :bar]
    

    This way you can overwrite methods in B and call the A version but if you don't overwrite it will still call the A version.

    You could also monkey patch the Module class if you'd like to make this functionality universal

    class Module
      def include_with_namespace(*mods)
        #Module#include runs in reverse so to maintain consistency my patch does as well
        mods.reverse.each do |mod|
          include mod
          (mod.instance_methods - Object.methods).each do |meth|
            alias_method "#{mod.name.downcase}_#{meth}", meth
          end
        end
      end
    end 
    

    Then this will work

    module C
      def foo(s)
        "C's foo with #{s}"
      end
      def see_me
        "You can see C"
      end 
    end
    
    module B;include_with_namespace A, C; end
    
    class Mixed;extend B;end
    
    Mixed.methods - Object.methods
    #=> [:a_foo, :a_bar, :c_foo,:c_see_me, :foo, :bar, :see_me]
    Mixed.foo("name")
    #=> "fooed with name"
    Mixed.c_foo("name")
    #=> "C's foo with name"