Project

General

Profile

Actions

Feature #22513

open

Allow Kernel#extend and Module#prepend to take a block

Feature #22513: Allow Kernel#extend and Module#prepend to take a block

Added by trans (Thomas Sawyer) about 13 hours ago. Updated about 8 hours ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:127359]

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.

obj.extend do
  def save
    log "saving #{id}"
    super
  end
end

Background

Both methods take only modules, and ignore a block. To add methods from a
block, the module has to be made first:

obj.extend(Module.new { ... })
# -or-
String.prepend(Module.new { ... })

That form also invites a common mistake. Without parentheses, do ... end
binds to extend, not to Module.new, so this silently defines nothing
(#5914):

obj.extend Module.new do
  def foo; end
end
obj.foo  # NoMethodError

Proposal

  • The block is evaluated as Module.new evaluates one: self is 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. After obj.extend(A, B) { ... },
    obj.singleton_class.ancestors starts [#<Class:obj>, #<Module>, A, B],
    and after C.prepend(A) { ... }, C.ancestors starts
    [#<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_object and
    extended, prepend_features and prepended), 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:

conn.extend do
  def query(sql)
    logger.debug(sql)
    super
  end
end

Wrapping a method of a class:

String.prepend do
  def upcase(*)
    super.tap { |s| log s }
  end
end

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 ... end mistake.
  • Ruby Facets has provided Kernel#extend with a block, implemented this
    way, since 2009.
  • A patch, with tests, is ready, and I'll link the pull request here.
Actions

Also available in: PDF Atom