Issue #22097 has been updated by Eregon (Benoit Daloze). Thank you for your replies, they address most of my concerns and show this was well though out. Yes, I think a performance warning is fine and important to add, nothing would be a silent performance trap. shugo (Shugo Maeda) wrote in #note-9:
The refinements also apply to methods defined with `def` inside the body
`def` is usually a lexical boundary but not for refinements (i.e., `using`), so I guess it makes some sense. I'm not sure it's really needed though and might be surprising, is it needed? I think for examples like ```ruby proc { Foo.class_eval do def bar "hi".shout end end Foo.new.bar }.with_refinements(StringRefinement) ``` it's quite strange and seem safer to not support that. This seems confusing, it looks like mixing `refine` and `using` together, even though it's only `using` and so defining a global method with some refinements applied. `module`/`class` would still be boundaries, right? Then the above would work but not this? ```ruby proc { class Foo def bar "hi".shout end end Foo.new.bar }.with_refinements(StringRefinement) ``` OTOH, I suppose `Proc#with_refinements` is basically doing the same as if the proc had `using M` on the first line, and the refinements wouldn't apply past the end of the proc. But I think going through `def` or `module/class` is the action-at-a-distance problem again, e.g. if the user defines some method inside a block (which at the place the block is declared is not clear at all it has refinements) then it unexpectedly get refinements too. I think we should limit it no nested blocks, those already inherit the local variables so it's natural. `define_method` would also not use those refinements. ---------------------------------------- Feature #22097: Add Proc#with_refinements https://bugs.ruby-lang.org/issues/22097#change-117721 * 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. ## Use Cases `Proc#with_refinements` is useful to implement internal DSLs applying refinements implicitly. * [activerecord-refinements](https://github.com/shugo/activerecord-refinements/blob/8869717fcba96fae3965d...) * DSL for SQL * Example: `User.where { :name == 'matz' }` * [radd_djur](https://github.com/shugo/radd_djur/blob/proc-with_refinements/lib/radd_djur/...) * DSL for monadic parser combinators ## 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 ``` ## 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/