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?