Contents

Is it possible to pass arguments to Ruby Modules?

Overview of simple and easy to use solutions to include Module into your Class with some parameters or arguments.

There is a hidden (or not obvious) power inside Ruby Modules, and I’ll try to show you some magic.

Did you know, that you can pass arguments while doing include MyModule, and even include same module several times with different parameters?

Some gems I know (dry-monads for example) uses following technique:

1
include MyModule[:some, :args]

Other gems (like concord or procto) doing it another way:

1
include MyModule.new(:some, :args)

Both of this parameterized module inclusion patterns has its pros and cons. Such hacks can increase complexity if done wrong, but also helps implement amazing custom-multi-module inclusion.

First option: def self.[]

Lets start with technique used by dry-monads. Reading the docs you can notice interesting pattern of including Monad related modules:

1
2
3
class SomeClass
  include Dry::Monads[:result]
end

What does it do? It accepts list of symbolized module names and includes them. If you need to include into your class Dry::Monads::Result, Dry::Monads::List, etc, it is as simple as:

1
2
3
class SomeClass
  include Dry::Monads[:result, :list]
end

And it will be equal to:

1
2
3
4
class SomeClass
  include Dry::Monads::Result
  include Dry::Monads::List
end

To understand how this works we need to look into the code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# @param [Array<Symbol>] monads
# @return [Module]
# @api public
def self.[](*monads)
  monads.sort!
  @mixins.fetch_or_store(monads.hash) do
    monads.each { |m| load_monad(m) }
    mixins = monads.map { |m| registry.fetch(m) }
    ::Module.new { include(*mixins) }.freeze
  end
end

Apart from gem specific actions, it is very simple. You need to define some method (#[] for example, but it can be anything, #call may suit you better), and this method should return anonymous module (look at ::Module.new {}).

This anonymous module can include necessary modules or define some methods (welcome to monkey patching).

Lets implement our own parameterized module. It should define method with method_name and output result.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
module SomeModule
  def self.define(method_name, result)
    Module.new do
      define_method method_name do
        puts result
      end
    end.freeze
  end
end

class SomeClass
  include SomeModule.define(:hello, :world)
end

It is synthetic example to get a gist of it. But it allows to call #hello on SomeClass instance:

1
2
> SomeClass.new.hello
world

Second option: Class Module

Another solution is to define Class as subclass of Module, and pass required arguments to initialize.

Lets take our previous synthetic example and implement it using Class inherited from Module:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class ModuleClass < Module
  def initialize(method_name, result)
    define_method method_name do
      puts result
    end
  end
  
  def self.call(method_name, result)
    new(method_name, result)
  end
end

You can use an initializer, but you also may use any class method like #call in example. This is how we include this type of Class Modules:

1
2
3
4
class SomeClass
  include ModuleClass.new(:hello, 'world')
  include ModuleClass.call(:foo, 'bar')
end

This pattern allows you to include your module class in several ways, try it and find what works best for you and your team.

1
2
3
4
> SomeClass.new.hello
world
> SomeClass.new.foo
bar

What’s the difference (pros and cons)?

If you need to dynamically include several other modules, like Dry Monads do, using anonymous modules via defining module singleton is a great solution. But if you need to do more complex stuff, like storing defined methods or callbacks registry, etc, Class is a friend to stick with.

If you look into ancestors of SomeClass, you’ll notice that anonymous module has its name for a reason: it is anonymous, and in SomeClass ancestors is listed as #Module:<0x00007ff431916ec8>. On the other hand, ModuleClass has it’s name and is listed in ancestors as ModuleClass:<0x00007ff431915b68>.

My thoughts are with ModuleClass solution it will be easier to debug and to write complex stuff for your needs.

What do you think?