Project

General

Profile

Feature #22097

Updated by shugo (Shugo Maeda) 17 days ago

## 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/8869717fcba96fae3965d2c37543bdf24068c774/lib/active_record/refinements.rb#L85-L86) [activerecord-refinements](https://github.com/shugo/activerecord-refinements/blob/4c811e2dc6c3abfc6576d540ef7515d56ba4a60b/lib/active_record/refinements.rb#L18) 
   * DSL for SQL 
   * Example: `User.where { :name == 'matz' }` 
 * [radd_djur](https://github.com/shugo/radd_djur/blob/proc-with_refinements/lib/radd_djur/grammar.rb#L172) 
   * 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| 

Back