Project

General

Profile

Actions

Feature #22097

open

Add Proc#with_refinements

Feature #22097: Add Proc#with_refinements
1

Added by shugo (Shugo Maeda) 6 days ago. Updated about 13 hours ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:125650]

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_refinements raises ArgumentError if the
    receiver is not created from a Ruby block.
: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.
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.
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

Related issues 2 (2 open0 closed)

Related to Ruby - Feature #16461: Proc#usingAssignedmatz (Yukihiro Matsumoto)Actions
Related to Ruby - Feature #12086: using: option for instance_eval etc.OpenActions
Actions

Also available in: PDF Atom