From: "Eregon (Benoit Daloze) via ruby-core" Date: 2026-06-11T21:34:12+00:00 Subject: [ruby-core:125709] [Ruby Feature#22097] Add Proc#with_refinements Issue #22097 has been updated by Eregon (Benoit Daloze). Since the performance relies on having `with_refinements` called always with the same Refinement module for a given block, how about raising an exception if it doesn't hold? Then we effectively have a guarantee vs very slow performance for e.g. `loop { original.with_refinements(A); original.with_refinements(B) }` (silly example, but could happen naturally in a bigger app). Semantically, nested blocks also get access to the refinements, as shown in `test_with_refinements_nested_block`, or for clarity: ```ruby module StringExt refine String do def shout = upcase + "!" end end original = ->(s) { -> { s.shout }.call } refined = original.with_refinements(StringExt) p refined.call("hello") # "HELLO!" ``` This is what I would expect, just I didn't see that in the description. Copying a block IR's, *and the IR of all nested blocks* (IR = bytecode for CRuby) is quite expensive. It's cached but it's still going to be a significant cost on either application startup/on the first request/etc. It would be good to get some numbers on that, e.g. creating and calling N blocks vs the same but also using `with_refinements`. The increased memory footprint would be worth documenting. Semantically, this means a given block (the lexical construct) can behave significantly differently based on calls to the original Proc or the `with_refinements` Proc. It's a bit like a given block being both a lambda and a proc, that's confusing and generally forbidden (except `send(rand < 0.5 ? :lambda : :proc) { ... }` but that's obvious; `lambda(&b)` is forbidden for this reason). Or similar to the issues we had with `Ractor.make_shareable` (which we solved by making the semantics much more similar and error if it would be too different). In summary: observable different semantics for the same block is always surprising, because hard to explain and to debug. IOW, it can break the author of the block's intention, by changing what a given piece of Ruby code means. I suppose the general expectation here is only the refined block is called and the original block is never called. If that holds I think it's fine, the problem is how to make it hold? To make the semantics cleaner, maybe we should prevent the original block to be called (i.e. raise an exception if it's called) once `with_refinements` has been called on it? (note: this would be stored in the block, so for all Proc instances of that block) One might still call the original block, then use `with_refinements` and observe the mixed semantics but that becomes a much narrower case. One way to fully address that would be to make this lexical, using a keyword or operator like: ```ruby proc_using_refinements(A) do ... end ``` and error if `proc_using_refinements` is not called with a literal block. Or maybe tweak the lambda operator like e.g.: ```ruby ->(s) [StringExt] { s.shout } ``` But I think the use case here wants more flexibility? ---------------------------------------- Feature #22097: Add Proc#with_refinements https://bugs.ruby-lang.org/issues/22097#change-117572 * Author: shugo (Shugo Maeda) * Status: Open ---------------------------------------- ## Abstract I propose `Proc#with_refinements(mod, ...)` to support block-level refinements. ```ruby 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_refinements` raises `ArgumentError` if the receiver is not created from a Ruby block. ```ruby :to_s.to_proc.with_refinements(StringExt) #=> ArgumentError ``` * Chained application of `Proc#with_refinements` is not allowed. `ArgumentError` is raised if the receiver is a `Proc` returned by `Proc#with_refinements`. ```ruby refined = prc.with_refinements(StringExt) refined.with_refinements(IntegerExt) #=> ArgumentError ``` * `define_method` (and `define_singleton_method`) rejects a `Proc` with refinements. `ArgumentError` is raised if the return value of `Proc#with_refinements` is given to `define_method`. ```ruby refined = prc.with_refinements(StringExt) define_method(:foo, &refined) #=> ArgumentError ``` * `Proc#with_refinements` can only be called from the main Ractor. `Ractor::IsolationError` is raised when called from a non-main Ractor. ```ruby Ractor.new { prc.with_refinements(StringExt) }.value #=> Ractor::IsolationError ``` ## 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_refinements` to `rb_proc_t`. * Added a hidden instance variable to `Proc` to store a `cref` with the applied refinements. * Added a single-entry cache `refinement_memo` to `rb_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_refinements` is a 1-bit field added to rb_proc_t's existing bit field, and `refinement_memo` shares a union with `mandatory_only_iseq` in rb_iseq_constant_body. * Execution speed: The common `Proc#call` path is kept frameless and only adds a single `has_refinements` bit check. * GC: The mark/free/memsize functions add a single branch per `iseq` to 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| -- 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/