Feature #22097
openAdd Proc#with_refinements
Description
Abstract¶
I propose Proc#with_refinements(mod, ...) to support block-level refinements.
module StringExt
refine String do
def shout = upcase + "!"
end
end
original = ->(s) { s.shout }
refined = original.with_refinements(StringExt)
p refined.call("hello") # "HELLO!"
p original.call("hello") # NoMethodError
When no argument is given, ArgumentError is raised.
When a non-Module argument is given, TypeError is raised.
Background and Motivation¶
I previously proposed Proc#using in [Feature #16461], but it introduced semantic complexities because it mutated existing blocks.
Instead of mutating the existing block, Proc#with_refinements returns a new Proc object with its own isolated call sites.
This approach makes its semantics much simpler than Proc#using, and it avoids thread-safety issues and plays nicely with inline caches.
Limitations¶
- Similar to
Proc#binding,Proc#with_refinementsraisesArgumentErrorif the
receiver is not created from a Ruby block.
:to_s.to_proc.with_refinements(StringExt) #=> ArgumentError
- Chained application of
Proc#with_refinementsis not allowed.ArgumentErroris
raised if the receiver is aProcreturned byProc#with_refinements.
refined = prc.with_refinements(StringExt)
refined.with_refinements(IntegerExt) #=> ArgumentError
-
define_method(anddefine_singleton_method) rejects aProcwith refinements.
ArgumentErroris raised if the return value ofProc#with_refinementsis given to
define_method.
refined = prc.with_refinements(StringExt)
define_method(:foo, &refined) #=> ArgumentError
Implementation¶
I've opened a pull request: https://github.com/ruby/ruby/pull/17248
A PoC for JRuby is also available at: https://github.com/jruby/jruby/pull/9486
Data structure changes¶
- Added a bit field
has_refinementstorb_proc_t. - Added a hidden instance variable to
Procto store acrefwith the applied refinements. - Added a single-entry cache
refinement_memotorb_iseq_constant_body.
Deep copy of iseq and caching¶
Proc#with_refinements performs a deep copy of the receiver's iseq to isolate its call sites from the original Proc.
While a deep copy can be an expensive operation, the single-entry cache in rb_iseq_constant_body mitigates this overhead effectively for most practical use cases where the same refinements are applied repeatedly.
Overhead for code not using Proc#with_refinements¶
- Memory footprint: Neither internal structure grows in size.
has_refinementsis a 1-bit field added to rb_proc_t's existing bit field, andrefinement_memoshares a union withmandatory_only_iseqin rb_iseq_constant_body. - Execution speed: The common
Proc#callpath is kept frameless and only adds a singlehas_refinementsbit check. - GC: The mark/free/memsize functions add a single branch per
iseqto select the union member.
Benchmark results: https://gist.github.com/shugo/ddfe92f28ea31e6527a2f270e6daee7c
Here's an excerpt from the results, where compare-ruby is master and built-ruby is the branch for this feature (focusing on Proc/Block operations):
| compare-ruby | built-ruby | |
|---|---|---|
| vm_proc | 47.215M | 46.149M |
| 1.02x | - | |
| vm_yield | 1.649 | 1.754 |
| - | 1.06x |