Inheriting class methods from / mixins modules in Ruby

In Ruby, class methods are known to inherit:

class P def self.mm; puts 'abc' end end class Q < P; end Q.mm # works 

However, it is unexpected for me that it does not work with mixins:

 module M def self.mm; puts 'mixin' end end class N; include M end M.mm # works N.mm # does not work! 

I know that the #extend method can do this:

 module X; def mm; puts 'extender' end end Y = Class.new.extend X X.mm # works 

But I'm writing mixin (or rather, I would like to write) containing both instance methods and class methods:

 module Common def self.class_method; puts "class method here" end def instance_method; puts "instance method here" end end 

Now I would like to do the following:

 class A; include Common # custom part for A end class B; include Common # custom part for B end 

I want A, B to inherit the instance and class methods from the Common module. But of course this does not work. So, isn't there a secret way to make this inheritance work in one module?

It seems to me impractical to break it into two different modules, one of which will include, and the other - to expand. Another possible solution would be to use the Common class instead of the module. But this is just a workaround. (What if there are two sets of common functionality Common1 and Common2 , and we really need to have mixins?) Is there any serious reason why method class inheritance does not work from mixins?

+61
ruby mixins
May 21 '12 at 21:34
source share
4 answers

A common idiom is to use the included hook and inject class methods from there.

 module Foo def self.included base base.send :include, InstanceMethods base.extend ClassMethods end module InstanceMethods def bar1 'bar1' end end module ClassMethods def bar2 'bar2' end end end class Test include Foo end Test.new.bar1 # => "bar1" Test.bar2 # => "bar2" 
+125
May 21 '12 at PM
source share

Here is a complete story explaining the necessary metaprogramming concepts needed to understand why module inclusion works the same way as in Ruby.

What happens when the module is turned on?

Inclusion of a module in a class adds a module to the ancestors of the class. You can look at the ancestors of any class or module by calling its ancestors method:

 module M def foo; "foo"; end end class C include M def bar; "bar"; end end C.ancestors #=> [C, M, Object, Kernel, BasicObject] # ^ look, it right here! 

When you call a method in an instance of C , Ruby will look at each element of this list of ancestors to find the instance method with the name provided. Since we included M in C , M now an ancestor of C , so when we call foo on an instance of C , Ruby will find this method in M :

 C.new.foo #=> "foo" 

Note that the inclusion does not copy any instance or class methods to the class - it simply adds a β€œnote” to the class, that it should also look for instance methods in the included module.

What about class methods in our module?

Since inclusion only changes the way methods of the instance are sent, including the module to the class , only makes its instance methods available to this class. Class methods and other declarations in the module are not automatically copied to the class:

 module M def instance_method "foo" end def self.class_method "bar" end end class C include M end M.class_method #=> "bar" C.new.instance_method #=> "foo" C.class_method #=> NoMethodError: undefined method `class_method' for C:Class 

How does Ruby implement class methods?

In Ruby, classes and modules are simple objects β€” they are instances of the Class and Module class. This means that you can dynamically create new classes, assign them to variables, etc.:

 klass = Class.new do def foo "foo" end end #=> #<Class:0x2b613d0> klass.new.foo #=> "foo" 

Also in Ruby, you have the option of defining what are called singleton methods for objects. These methods are added as new instance methods to the special hidden singleton class of the object:

 obj = Object.new # define singleton method def obj.foo "foo" end # here is our singleton method, on the singleton class of `obj`: obj.singleton_class.instance_methods(false) #=> [:foo] 

But aren't classes and modules just simple objects? In fact they are! Does this mean that they can have solid methods? Yes! This is how class methods are born:

 class Abc end # define singleton method def Abc.foo "foo" end Abc.singleton_class.instance_methods(false) #=> [:foo] 

Or a more common way to define a class method is to use self in the class definition block, which refers to the created class object:

 class Abc def self.foo "foo" end end Abc.singleton_class.instance_methods(false) #=> [:foo] 

How to include class methods in a module?

As we just established, class methods are really just instance methods for a singleton class of a class object. Does this mean that we can just include the module in a singleton class to add a bunch of class methods? Yes it is!

 module M def new_instance_method; "hi"; end module ClassMethods def new_class_method; "hello"; end end end class HostKlass include M self.singleton_class.include M::ClassMethods end HostKlass.new_class_method #=> "hello" 

This line self.singleton_class.include M::ClassMethods does not look very good, so Ruby added Object#extend , which does the same - that is, it includes the module in a singleton object class:

 class HostKlass include M extend M::ClassMethods end HostKlass.singleton_class.included_modules #=> [M::ClassMethods, Kernel] # ^ there it is! 

Moving an extend call to a module

This previous example is not well-structured code for two reasons:

  • Now we need to call both include and extend in the HostClass definition to enable our module correctly. This can be very cumbersome if you have to enable many of these modules.
  • HostClass directly references M::ClassMethods , which is a detail of an implementation of the M module that HostClass should not know or care.

So, how about this: when we call include on the first line, we somehow notify the module that it was turned on, and also give it our class object so that it can call extend . So this is a module job to add class methods if it wants to.

This is exactly what the special self.included method is for . Ruby automatically calls this method whenever a module is included in another class (or module) and passes the first argument to the host class object:

 module M def new_instance_method; "hi"; end def self.included(base) # `base` is `HostClass` in our case base.extend ClassMethods end module ClassMethods def new_class_method; "hello"; end end end class HostKlass include M def self.existing_class_method; "cool"; end end HostKlass.singleton_class.included_modules #=> [M::ClassMethods, Kernel] # ^ still there! 

Of course, adding class methods is not the only thing we can do in self.included . We have a class object, so we can call any other (class) method on it:

 def self.included(base) # `base` is `HostClass` in our case base.existing_class_method #=> "cool" end 
+8
Jul 16 '17 at 10:10
source share

As Sergio said in the comments, for guys who are already on Rails (or don't mind depending on Active Support ), Concern is useful here:

 require 'active_support/concern' module Common extend ActiveSupport::Concern def instance_method puts "instance method here" end class_methods do def class_method puts "class method here" end end end class A include Common end 
+2
Jun 23 '17 at 3:36 on
source share

You can get your cake and eat it by doing this:

 module M def self.included(base) base.class_eval do # do anything you would do at class level def self.doit #class method @@fred = "Flintstone" "class method doit called" end # class method define def doit(str) #instance method @@common_var = "all instances" @instance_var = str "instance method doit called" end def get_them [@@common_var,@instance_var,@@fred] end end # class_eval end # included end # module class F; end F.include M F.doit # >> "class method doit called" a = F.new b = F.new a.doit("Yo") # "instance method doit called" b.doit("Ho") # "instance method doit called" a.get_them # >> ["all instances", "Yo", "Flintstone"] b.get_them # >> ["all instances", "Ho", "Flintstone"] 

If you intend to add an instance and class variables, you will end up pulling your hair out when you come across a bunch of broken code if you don't do it that way.

0
Dec 12 '17 at 19:06
source share



All Articles