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:
include MyModule[:some, :args]
Other gems (like concord or procto) doing it another way:
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:
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:
class SomeClass
include Dry::Monads[:result, :list]
end
And it will be equal to:
class SomeClass
include Dry::Monads::Result
include Dry::Monads::List
end
To understand how this works we need to look into the code
# @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
.
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:
> 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:
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:
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.
> 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?