From: "trans (Thomas Sawyer) via ruby-core" Date: 2026-11-08T21:13:53+00:00 Subject: [ruby-core:127363] [Ruby Feature#22513] Allow Kernel#extend and Module#prepend to take a block Issue #22513 has been updated by trans (Thomas Sawyer). Pull request with the patch and tests: https://github.com/ruby/ruby/pull/19774 It has one commit for `Kernel#extend` and one for `Module#prepend`. ---------------------------------------- Feature #22513: Allow Kernel#extend and Module#prepend to take a block https://bugs.ruby-lang.org/issues/22513#change-119794 * Author: trans (Thomas Sawyer) * Status: Open ---------------------------------------- # 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. ```ruby 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: ```ruby 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): ```ruby 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 `[#, #, A, B]`, and after `C.prepend(A) { ... }`, `C.ancestors` starts `[#, 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: ```ruby conn.extend do def query(sql) logger.debug(sql) super end end ``` Wrapping a method of a class: ```ruby 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. -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/