How to nest the inclusion of modules when using the Ruby on Rails ActiveSupport::Concern feature?
If you include MyModuleB
in the "body" of MyModuleA
, then it is the module itself that is extended with B's functionality. If you include it in the included
block, then it is included on the class that mixes in MyModuleA
.
That is:
module MyModuleA
extend ActiveSupport::Concern
include MyModuleB
end
produces something like:
MyModuleA.send :include, MyModuleB
class Foo
include MyModuleA
end
while
module MyModuleA
extend ActiveSupport::Concern
included do
include MyModuleB
end
end
produces something like:
class Foo
include MyModuleA
include MyModuleB
end
The reason for this is that ActiveSupport::Concern::included
is analogous to:
def MyModuleA
def self.included(klass, &block)
klass.instance_eval(&block)
end
end
The code in the included
block is run in the context of the including class, rather than the context of the module. Thus, if MyModuleB needs access to the class it's being mixed-in to, then you'd want to run it in the included
block. Otherwise, it's effectively the same thing.
By means of demonstration:
module A
def self.included(other)
other.send :include, B
end
end
module B
def self.included(other)
puts "B was included on #{other.inspect}"
end
end
module C
include B
end
class Foo
include A
end
# Output:
# B was included on C
# B was included on Foo
How to make methods added to a class by including nested modules to be instance methods of that class when using the ActiveSupport::Concern feature?
Methods in modules that get mixed into a class become instance methods on that class. While putting them in the included
block would likely work, there's no need to do it. This, by extension, works with modules, since you can include ModuleB
in ModuleA
and all its instance methods become instance methods on ModuleA
, and once ModuleA
is included on class Foo
, all its instance methods (including those mixed in from B
) become instance methods on Foo.
A "traditional" mix-in looks like this:
module Mixin
def self.included(klass)
klass.send :extend, ClassMethods
klass.some_class_method
end
module ClassMethods
def some_class_method
puts "I am a class method on #{self.inspect}"
end
end
def some_instance_method
puts "I am an instance method on #{self.inspect}"
end
end
class Foo
include Mixin
end
Foo.new.some_instance_method
# Output:
# I am a class method on Foo
# I am an instance method on #<Foo:0x00000002b337e0>
ActiveSupport::Concern just pretties this up a bit by automatically including a module named ClassMethods
and by running the included
block in the context of the including class, so the equivalent is:
module Mixin
extend ActiveSupport::Concern
included do
some_class_method
end
module ClassMethods
def some_class_method
puts "I am a class method on #{self.inspect}"
end
end
def some_instance_method
puts "I am an instance method on #{self.inspect}"
end
end
class Foo
include Mixin
end
Foo.new.some_instance_method
# Output:
# I am a class method on Foo
# I am an instance method on #<Foo:0x000000034d7cd8>
Extending ActiveSupport::Concern
You shouldn't override or extend directly the concern. With a simple module it would be maybe useful, but concerns are set up to be explicitly extended:
module MyFoo
extend ActiveSupport::Concern
extend Foo
included do
#some other stuff
end
def method_b
end
end
included do ..... end (wrong no of arguments) error
oh.... got it fixed...
Stupid typo mistake
extend ActiveSupport::Concerns should be Concern
Getting around ruby-units conflict with activesupport
The answer to this is to require the ruby-units library before the rails library in the Gemfile:
gem 'ruby-units'
gem 'rails'
Obviously you then won't be able to use .to() on strings to access the ruby-units conversion.
Understanding the singleton class when aliasing a instance method
I recommend Yehuda Katz's post on metaprogamming on Ruby's self. Here's my humble summary in response to your question:
In Ruby, all objects have a singleton class (also known as metaclass). Objects inherit first from their singleton class invisibly, then from their explicit class. Ruby classes themselves have their own singleton classes since classes are objects as well. The class <<
idiom is simply Ruby's syntax for accessing the scope of an object's singleton class.
class Person
class << self
# self in this scope is Person's singleton class
end
end
person = Person.new
person_singleton_class = class << person; self; end
Your version of Rails actually provides singleton_class
as a shortcut. Since singleton_class
is an available method, you don't need to assign it to a variable in the expression singleton_class = class << self; self end
:
Person.singleton_class
person = Person.new
person.singleton_class
Since a class inherits directly from its singleton class, this is where we want to add class methods dynamically when metaprogramming. Ruby provides a few ways to open up the scope of an object while maintaining access to the surrounding scope: class_eval
and instance_eval
. There are subtle differences in the way these behave (Yehuda's post explains this), but you may use either to enter the scope of your singleton class, resolve methods on the singleton class as self
and still have access to my_method_name
from the surrounding scope.
All that said, you could make a few small changes to your module:
module MyModule
extend ActiveSupport::Concern
included do
# Builds the instance method name.
my_method_name = build_method_name.to_sym # => :my_method
# Defines the :my_method instance method in the including class of MyModule.
define_singleton_method(my_method_name) do |*args|
# ...
end
singleton_class.class_eval do
# method resolution in scope of singleton class
alias_method :my_new_method, my_method_name
end
end
end
How to dynamically open a class so to add to it a scope method that makes use of a local variable?
counter_cache_column
is a local variable. Local variable are local to the scope they are defined in (that's why they are called local variables).
In this case, the scope is the block passed to included
.
The class definition and the method definition create a new empty scope. Only blocks create nested scopes, so, you need to use a block to defined your method. Thankfully, there is a way to do so: by passing a block to define_method
:
module MyModule
extend ActiveSupport::Concern
included do
klass = get_class_name.constantize # => User
counter_cache_column = get_counter_cache # => "counter_count"
klass.define_singleton_method(:order_by_counter) {
order("#{counter_cache_column} DESC")
}
end
end
I made some other style improvements:
self
is the implicit receiver in Ruby, there is no need to specify itCLASS_NAME
is misleading: it doesn't contain the name of the class, it contains the class itself- also, I don't see why it would need to be a constant
Code refactoring by injecting a Hash
What you should write in the inject
block:
result.update(key => IVar.new(key, opts, args.keys)
But no need to use inject
or each_with_object
to build a hash, we have Hash[pairs]
:
@i_vars = Hash[(args || {}).map { |k, opts| [k, IVar.new(k, opts, (args || {}).keys)] }
However, I wouldn't obsess with one-liners, I'd write an equivalent but more clear code:
hargs = args || {}
@i_vars = Hash[hargs.map do |key, opts|
[key, IVar.new(key, opts, hargs.keys)]
end]
Related Topics
Where Do You Put CSS Files in a Rails App Directory
How to Ensure a Rake Task Only Running a Process at a Time
What Does It Mean Bundle_Disable_Shared_Gems: '1'
Mocking Chain of Methods in Rspec
Different Background Color for Different Pages in Rails
How to Set Ruby Environment in Linux
Sinatra - How to Get the Server's Domain Name
Ssl Error on Http Post (Unknown Protocol)
Make Text Input Fields and Their Labels Line Up Correctly
Ruby Time.Parse Gives Me Out of Range Error
Ruby Array Concat Versus + Speed
Access Localhost on MAC from Xcode? Phonegap Communicating with Ajax to a Local Rails App
Finding the Difference Between Strings in Ruby
Ruby: Controlling Printing in Scientific Notation