Modules
Modules serve two purposes:
- as namespaces for defining other types, methods and constants
- as partial types that can be mixed in other types
An example of a module as a namespace:
module Curses
class Window
end
end
Curses::Window.new
Library authors are advised to put their definitions inside a module to avoid name clashes. The standard library usually doesn't have a namespace as its types and methods are very common, to avoid writing long names.
To use a module as a partial type you use include
or extend
.
An include
makes a type include methods defined in that module as instance methods:
module ItemsSize
def size
items.size
end
end
class Items
include ItemsSize
def items
[1, 2, 3]
end
end
items = Items.new
items.size # => 3
In the above example, it is as if we pasted the size
method from the module into the Items
class. The way this really works is by making each type have a list of ancestors, or parents. By default this list starts with the superclass. As modules are included they are prepended to this list. When a method is not found in a type it is looked up in this list. When you invoke super
, the first type in this ancestors list is used.
A module
can include other modules, so when a method is not found in it it will be looked up in the included modules.
An extend
makes a type include methods defined in that module as class methods:
module SomeSize
def size
3
end
end
class Items
extend SomeSize
end
Items.size # => 3
Both include
and extend
make constants defined in the module available to the including/extending type.
Both of them can be used at the top level to avoid writing a namespace over and over (although the chances of name clashes increase):
module SomeModule
class SomeType
end
def some_method
1
end
end
include SomeModule
SomeType.new # OK, same as SomeModule::SomeType
some_method # OK, 1
extend self
A common pattern for modules is extend self
:
module Base64
extend self
def encode64(string)
# ...
end
def decode64(string)
# ...
end
end
In this way a module can be used as a namespace:
Base64.encode64 "hello" # => "aGVsbG8="
But also it can be included in the program and its methods can be invoked without a namespace:
include Base64
encode64 "hello" # => "aGVsbG8="
For this to be useful the method name should have some reference to the module, otherwise chances of name clashes are high.
A module cannot be instantiated:
module Moo
end
Moo.new # undefined method 'new' for Moo:Module
Module Type Checking
Modules can also be used for type checking.
If we define two modules with names A
and B
:
module A; end
module B; end
These can be included into classes:
class One
include A
end
class Two
include B
end
class Three < Two
include A
end
We can then type check against instances of these classes with not only their class, but the included modules as well:
one = One.new
typeof(one) # => One
one.is_a?(A) # => true
one.is_a?(B) # => false
three = Three.new
typeof(three) # => Three
three.is_a?(A) # => true
three.is_a?(B) # => true
This allows you to define arrays and methods based on module type instead of class:
one = One.new
two = Two.new
three = Three.new
new_array = Array(A).new
new_array << one # Ok, One inherits module A
new_array << three # Ok, Three includes module A
new_array << two # Error, because Two does not inherit module A
To the extent possible under law, the persons who contributed to this workhave waived
all copyright and related or neighboring rights to this workby associating CC0 with it.
https://crystal-lang.org/reference/syntax_and_semantics/modules.html