What is Ruby module.included?
Asked Answered
L

1

7

I'm trying to better my understanding of meta-programming in Ruby and am confused as to what Module.included is? My current understanding is that this is a callback invoked by Ruby whenever the module is included into another module or class. Other than that, what types of (meta-)programming constructs are these used in? Any examples?

Ladida answered 3/5, 2020 at 22:46 Comment(2)
When asking an extremely broad, open-ended question like "what is x?" it helps to state what your understanding is to give a starting point.Birchard
Thanks! I've updated the question to reflect my current understanding.Ladida
U
14

Module#included allows modules to inject both class and instance methods, and execute associated code, from a single include.

The documentation for ActiveSupport::Concern illustrates a typical use. It's injecting class methods into the calling class, and executing code. In this case, adding a scope.

module M
  def self.included(base)
    base.extend ClassMethods
    base.class_eval do
      scope :disabled, -> { where(disabled: true) }
    end
  end

  module ClassMethods
    ...
  end
end

And here's the ActiveSupport::Concern version which does the same thing, but adds declarative syntax sugar.

require 'active_support/concern'

module M
  extend ActiveSupport::Concern

  included do
    scope :disabled, -> { where(disabled: true) }
  end

  class_methods do
    ...
  end
end

With included a class simply includes the module. It allows the module to be one neat package: instance methods, class methods, and setup code.

class Thing
  # Class and instance methods are injected, and the new scope is added.
  include M
end

Without included a module can only inject instance methods. Class methods would have to be added separately, as well as executing any setup code.

module M
  def some_instance_method
    ...
  end

  module ClassMethods
    def setup
      scope :disabled, -> { where(disabled: true) }
    end
  end
end
class Thing
  # Inject the instance methods
  include M

  # Inject the class methods
  extend M::ClassMethods

  # Run any setup code.
  setup
end

Other examples might be registering a class, for example as an available plugin.

module Plugin
  def self.included(base)
    base.extend ClassMethods
    base.class_eval do
      register_as_plugin(base)
    end
  end

  module ClassMethods
    def register_as_plugin(klass)
      ...
    end
  end
end

class Thing
  include Plugin
end

Or adding necessary accessors.

module HasLogger
  def self.included(base)
    base.class_eval do
      attr_writer :logger
    end
  end

  def logger
    @logger ||= Rails.logger
  end
end

class Thing
  include HasLogger
end
Upgrade answered 3/5, 2020 at 23:27 Comment(2)
why we need base. and base.class_eval ?Corrective
@Corrective Because included is called on the module, the class which included it is passed in as base. In the HasLogger example, inside self.included self is HasLogger and base is Thing. If it did attr_writer :logger without the base.class_eval it would add the attribute to HasLogger. We want to add the attribute to Thing. base.class_eval lets us do that; inside base.class_eval self is Thing. We could also have written base.attr_writter :logger.Upgrade

© 2022 - 2024 — McMap. All rights reserved.