When modules are included (or extended or prepended) into an class the traditional approach doesn’t allow arguments to be provided. Why would you want arguments on an include? Let’s work with a concrete example. Below is a simple “slug” implementation for ActiveRecord objects:

module Slug

def to_param

name.downcase.gsub /\W+/, '-'

end

end

The problem with this module is that it is brittle. It assumes the value we want to slugify is always `name`. We could make this a bit more flexible with this implementation:

module Slug

def to_param

slug_field.downcase.gsub /\W+/, '-'

end private def slug_field

name

end

end

This sort of callback approach works. For example:

class BlogPost < ApplicationRecord

include Slug private def slug_field

title

end

end

The problem is that the callback seems disconnected from the include as you add more to the file. Especially when the callback and include are not so obviously connected (here they share the keyword slug). It would be better if we could pass arguments during the include to configure it. My ideal syntax would be:

class BlogPost < ApplicationRecord

include Slug, field: :title

end

Unfortunately Ruby doesn’t support arguments with includes, but by using anonymous modules we can emulate the desired behavior. Although we don’t get exactly the above syntax we get something close:

class BlogPost < ApplicationRecord

include Slug.new field: title

end

So what does a arg-enabled `Slug` module now look like?

class Slug < Module

def initialize field: :name

super() do

define_method :to_param do

public_send(field).downcase.gsub /\W+/, '-'

end

end

end

end

Obviously our Slug module has gotten a bit more complicated, but to the advantage of the code using our module, a trade-off often worth making. Let’s break this down to understand how this works.

Syntactical Sugar

Modules are just instances of the class Module assigned to a constant. The following are the same:

module Foo

end Foo = Module.new

Anonymous/Local Modules

Modules don’t need to be assigned to a constant. They can remain anonymous or assigned to a local variable:

def foo

bar = Module.new

end

Above we create a new module but instead of assigning it to a global constant we assign it to a local variable so it only exists inside the method `foo`. After executing `foo` it is available for garbage collection just like any other object instance.

Defining Mixin Methods for Modules

`Module#new` accepts a block and the methods defined in that block become the methods for that module to be mixed into the class including that module. The following are the same:

module Foo

def bar

puts 'baz'

end

end Foo = Module.new do

def bar

puts 'baz'

end

end

We can also use `define_method` if we want our method definition to be a block.

Foo = Module.new do

define_method :bar do

puts 'baz'

end

end

This is logically the same as the previous examples. While more verbose it does give us an advantage. Blocks create closures to capture local variables. We will see how to use that later.

Subclassing Modules

Since modules are just instances of the class Module, you can subclass it to provide specializations. The below is logically equivalent to the previous examples:

class ModuleWithBar < Module

def initialize

super do

def bar

puts 'baz'

end

end

end

end Foo = ModuleWithBar.new

Rather than define `bar` in a block given to `Module#new`, we are subclassing Module so that the block with `bar` is automatically provided anytime a `ModuleWithBar` is created.

Passing Arguments

You can pass arguments to your subclass `initialize` method just like any `initialize` method. Since the superclass doesn’t accept arguments you must explicitly call `super` without arguments to avoid the arguments you added from moving up the chain. Arguments can be positional, have defaults, be keyword arguments, etc.

class ModuleWithArgs < Module

def initialize arg='default'

super()

end

end

In our above example we didn’t actually use the argument. Here is where our previous discussion of `define_method` comes in handy.

If we just use `def` when defining our methods they just execute in the scope of the included object. But, if we use `define_method` then our block is executed in the scope of the included object but also has access to the local variables in the closure created. This allows our method to use the arguments. So:

class ModuleWithArgs < Module

def initialize arg='default'

super() do

define_method :bar do

puts arg

end

end

end

end

If we include an anonymous instance of our module and supply no arguments we get the following:

class Foo

include ModuleWithArgs.new

end Foo.new.bar # prints 'default'

But instead if we use the arg we get to configure the behavior of our include:

class Cat

include ModuleWithArgs.new 'hello'

end Cat.new.bar # prints 'hello'

Wrap Up

Let’s use all this info to circle back to our implementation of Slug:

class Slug < Module

def initialize field: :name

super() do

define_method :to_param do

public_send(field).downcase.gsub /\W+/, '-'

end

end

end

end

They key points here are: