Feature #22513
openAllow Kernel#extend and Module#prepend to take a block
Description
Abstract¶
Let Kernel#extend and Module#prepend take a block. The block is
evaluated in a new anonymous module, as Module.new evaluates its block,
and the receiver is extended with, or prepended with, that module.
Background¶
Both methods take only modules, and ignore a block. To add methods from a
block, the module has to be made first:
That form also invites a common mistake. Without parentheses, do ... end
binds to extend, not to Module.new, so this silently defines nothing
(#5914):
Proposal¶
- The block is evaluated as
Module.newevaluates one:selfis the new
module, which is also passed as the block's argument. - Modules may still be given. The block's module is added after them, so
its methods take precedence. Afterobj.extend(A, B) { ... },
obj.singleton_class.ancestorsstarts[#<Class:obj>, #<Module>, A, B],
and afterC.prepend(A) { ... },C.ancestorsstarts
[#<Module>, A, C]. That is how a class body's methods take precedence
over the modules it includes. - With a block, the modules are optional. Without one, an argument is still
required. - The block is evaluated before any module is added, after the arguments
are checked, so an error in the block leaves the receiver unchanged. - The usual hooks run on the block's module (
extend_objectand
extended,prepend_featuresandprepended), and the receiver is
returned as before.
Use cases¶
extend and prepend both put a module in front of existing methods, so
a method in the module can call super to reach the one it replaces. That
is most of what they're used for, and a block lets it be written in one
step, where it's used.
Adding to or wrapping the behavior of one object, such as a test double or
an object configured by a DSL:
Wrapping a method of a class:
Discussion¶
This isn't a large feature, but it's the one obvious meaning a block could
have for these methods. It also turns the mistake shown above into code
that does what its writer meant.
include is left out on purpose. Its module goes behind the class, so the
class's own methods would override whatever the block defines.
Compatibility: both methods currently ignore a block, so code that passes
one, mostly the mistake above, would now have its block evaluated. Nothing
in Ruby's own lib, ext, test or spec directories passes a block to either
method.
#8046 stalled on whether the block should be evaluated in the singleton
class or in a new module. As nobu pointed out there, without a module it
would not be #extend but #singleton_class_eval, so this proposal uses a
module.
See also¶
- #8046, "allow Object#extend to take a block" (2013), and #8038, where
Matz said: "Making Object#extend to take a block is interesting idea. We
will discuss later if it's submitted." - #5914, the
extend Module.new do ... endmistake. - Ruby Facets has provided
Kernel#extendwith a block, implemented this
way, since 2009. - A patch, with tests, is ready, and I'll link the pull request here.