From: "ko1 (Koichi Sasada) via ruby-core" Date: 2025-08-27T08:46:04+00:00 Subject: [ruby-core:123084] [Ruby Feature#21550] Ractor.sharable_proc/sharable_lambda to make sharable Proc object Issue #21550 has been updated by ko1 (Koichi Sasada). Eregon (Benoit Daloze) wrote in #note-6: > I wonder if the ability to capture outer local variables for shareable procs is even needed at all. > Because that only works if the value of the captured variables is shareable, which seems not so likely (only numbers/true/false/nil/frozen-string-literals/modules/regexps are frozen/shareable without an explicit freeze/make_shareable call). > I would like to see some concrete examples of blocks to be made shareable which capture outer local variables, and how they handle the case of captured variable values not already being deeply frozen (cc @tenderlovemaking you said there were some in Rails IIRC). I have two examples. One is `define_method`. ```ruby def define_foo suffix, ivar_name # should be in Symbol define_method "foo_#{mid} do instance_variable_get ivar_name end end ``` This method define a new method by using a outer local variable `ivar_name`. For example, this pattern is used: ```ruby # lib/delegate.rb def Delegator.delegating_block(mid) # :nodoc: lambda do |*args, &block| target = self.__getobj__ target.__send__(mid, *args, &block) end.ruby2_keywords end # lib/bundler/errors.rb def self.status_code(code) define_method(:status_code) { code } # repl_type_completor/test/repl_type_completor/test_repl_type_completor.rb def with_failing_method(klass, method_name, message) ... # message should be marked as sharable klass.define_method(method_name) do |*, **| raise Exception.new(message) end # debug/test/console/config_fork_test.rb ['fork', 'Process.fork', 'Kernel.fork'].each{|fork_method| c = Class.new ConsoleTestCase do ... # fork_method shouldbe marked as sharable define_method :fork_method do fork_method end end ``` The first motivation is how to handle `define_method`. Another example to make a task object which should be run in another Ractor. result = Ractor::Port.new Ractor.sharable_proc do result << task() end p result.receive ``` I have already written the following pattern many times: ```ruby port = Ractor::Port.new Ractor.new port do |port| port << ... end ``` and it is somewhat cumbersome to write. ---- At least we have no objection to introduce `Ractor.shareable_proc(&bl)` if `bl` doesn't have references to the outer variable. I'll merge it. ---- I have another idea. Returning to the issue: we want a way to express that, within a block, an outer variable is shadowed while preserving its current value. We already have syntax to shadow an outer variable using `|i; a|`, where `a` is shadowed in the block and initialized to `nil` (just like a normal local variable). ```ruby a = 42 pr = proc{|;a| p a} a = 43 pr.call #=> nil ``` What if we instead initialized the shadowed variable to the outer variable's current value? ```ruby a = 42 pr = proc{|;a| p a} a = 43 pr.call #=> 42 ``` Maybe only few people know this spec and I checked that there are few lines in rubygems (78 cases in 3M files)(*1). So I think there is a few compatibility impact. *1: ``` # {name: foo}, 3] means shadowed name "foo" is used 3 times. [[[:FILES, 3_068_818], [:FAILED_PARSE, 14_928]], [[:lvar, 78], [{name: :glark}, 6], [[:FAILED, SystemStackError], 5], [{name: :bl}, 4], [{name: :a}, 4], [{name: :options}, 3], [{name: :name}, 3], [{name: :bytes}, 2], [{name: :extra}, 2], [{name: :shape}, 2], [{name: :b}, 2], [{name: :x}, 2], [{name: :new_root}, 2], [{name: :bl2}, 2], [{name: :ifc_spc}, 2], [{name: :m}, 2], [{name: :foo}, 2], [{name: :key}, 2], [{name: :md}, 2], [{name: :d}, 2], [{name: :c}, 2], [{name: :out}, 2], [{name: :bar}, 2], [{name: :req}, 1], [{name: :resp}, 1], [{name: :spec}, 1], [{name: :world}, 1], [{name: :u}, 1], [{name: :onto}, 1], [{name: :rbname}, 1], [{name: :r}, 1], [{name: :h}, 1], [{name: :bug9605}, 1], [{name: :expected}, 1], [{name: :result}, 1], [{name: :services}, 1], [{name: :step}, 1], [{name: :obj}, 1], [{name: :ancestor}, 1], [{name: :count}, 1], [{name: :path}, 1], [{name: :nav}, 1], [{name: :safe_position_in_input}, 1], [{name: :safe_count}, 1], [{name: :v}, 1], [{name: :k}, 1], [{name: :indent}, 1], [{name: :y}, 1], [{name: :z_diff}, 1]]] ``` ---------------------------------------- Feature #21550: Ractor.sharable_proc/sharable_lambda to make sharable Proc object https://bugs.ruby-lang.org/issues/21550#change-114397 * Author: ko1 (Koichi Sasada) * Status: Open * Assignee: ko1 (Koichi Sasada) * Target version: 3.5 ---------------------------------------- Let's introduce a way to make a sharable Proc. * `Ractor.shareable_proc(self: nil, &block)` makes proc. * `Ractor.shareable_lambda(self: nil, &block)` makes lambda. See also: https://bugs.ruby-lang.org/issues/21039 ## Background ### Motivation Being able to create a shareable Proc is important for Ractors. For example, we often want to send a task to another Ractor: ```ruby worker = Ractor.new do while task = Ractor.receive task.call(...) end end task = (sharable_proc) worker << task task = (sharable_proc) worker << task task = (sharable_proc) worker << task ``` There are various ways to represent a task, but using a Proc is straightforward. However, to make a Proc shareable today, self must also be shareable, which leads to patterns like: ```ruby nil.instance_eval{ Proc.new{ ... } } ``` This is noisy and cryptic. We propose dedicated methods to create shareable Proc objects directly. ## Specification * `Ractor.shareable_proc(self: nil, &block)` makes a proc. * `Ractor.shareable_lambda(self: nil, &block)` makes a lambda. Both methods create the Proc/lambda with the given self and make the resulting object shareable. Captured outer variables follow the current `Ractor.make_shareable` semantics: * If a captured outer local variable refers to a shareable object, a shareable Proc may read it. * If any captured outer variable refers to a non���shareable object, creating the shareable Proc raises an error. ```ruby a = 42 b = "str" Ractor.sharalbe_proc{ p a #=> 42 } Ractor.sharalbe_proc{ # error when making a sharealbe proc p b #=> 42 } ``` * The captured outer local variables are copied by value when the shareable Proc is created. Subsequent modifications of those variables in the creator scope do not affect the Proc. ```ruby a = 42 shpr = Ractor.sharable_proc{ p a } a = 0 shpr.call #=> 42 ``` * Assigning to outer local variables from within the shareable Proc is not allowed (error at creation). ```ruby a = 42 Ractor.shareable_proc{ # error when making a sharealbe proc a = 43 } ``` More about outer-variable handling are discussed below. In other words, from the perspective of a shareable Proc, captured outer locals are read���only constants. This proposal does not change the semantics of Ractor.make_shareable() itself. ## Discussion about outer local variables [Feature #21039] discusses how captured variables should be handled. I propose two options. ### 1. No problem to change the outer-variable semantics @Eregon noted that the current behavior of `Ractor.make_shareable(proc_obj)` can surprise users. While that is understandable, Ruby already has similar *surprises*. For instance: ```ruby RSpec.describe 'foo' do p self #=> RSpec::ExampleGroups::Foo end ``` Here, `self` is implicitly replaced, likely via `instance_exec`. This can be surprising if one does not know self can change, yet it is accepted in Ruby. We view the current situation as a similar kind of surprise. ### 2. Enforce a strict rule for non���lexical usage The difficulty is that it is hard to know which block will become shareable unless it is lexically usage. ```ruby # (1) On this code, it is clear that the block will be shareable block: a = 42 Ractor.sharable_proc{ p a } # (2) On this code, it is not clear that the block becomes sharable or not get path do p a end # (3) On this code, it has no problem because get '/hello' do "world" end ``` The idea is to allow accessing captured outer variables only for lexically explicit uses of `Ractor.shareable_proc` as in (1), and to raise an error for non���lexical cases as in (2). So the example (3) is allowed if the block becomes sharable or not. The strict rule is same as `Ractor.new` block rule. -- 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/