From: "Eregon (Benoit Daloze) via ruby-core" Date: 2025-08-28T08:07:46+00:00 Subject: [ruby-core:123103] [Ruby Feature#21550] Ractor.sharable_proc/sharable_lambda to make sharable Proc object Issue #21550 has been updated by Eregon (Benoit Daloze). ko1 (Koichi Sasada) wrote in #note-14: > @Eregon let's me clarify your proposal. > > * Option 4 is your proposal on https://bugs.ruby-lang.org/issues/21039#note-21, which prohibit any writing to the captured outer variables from inside/outside of the block I think there is a misunderstanding there: Option 4 does *not* prohibit writing to captured variables, it never proposed that. Ruby code can write to all local variables, always. Maybe the text in that comment is confusing (e.g. "Disallow writes" is not to literally disallow writing, it's to prevent making a shareable proc in such a context). Please read https://bugs.ruby-lang.org/issues/21550#note-12 and ignored that older comment, it should be very clear what is proposed with all the examples. It prevents making a `shareable_proc` for a block which uses captured variables which are reassigned *after* the block, as I showed in the examples above: ```ruby def example a = 1 b = proc { a } Ractor.shareable_proc(&b) # should be Ractor::IsolationError: cannot isolate a block because it accesses outer variables (a) which are reassigned inside or after the block a = 2 end ``` All options already prevent making a `shareable_proc` for a block which uses captured variables which are reassigned *inside* the block. ```ruby # error (that everyone seems to agree on) def example a = 1 b = proc { a = 2 } Ractor.shareable_proc(&b) end ``` So option 4 does the same but also prevents making a shareable proc when reassigned *after*. > * This proposal is clearly rejected by Matz https://bugs.ruby-lang.org/issues/21039#note-28 I think the proposal was misunderstood, I tried to clarify in https://bugs.ruby-lang.org/issues/21039#note-33 as well. "prohibiting outer assignment to local variables when a proc is made sharable." was never proposed (because that's clearly too difficult and the wrong place to prevent/prohibit). > * About `define_method` > * > That would mean that define_method would automatically shallow-copy the environment, Ractor or not, for consistency. That's a semantic change but it seems very compatible. > * I think it is not acceptable because it will break compatibility if the block uses outer local variables as a storage, like that: My last comment explains how to fix this. Since it's not clear I will make a separate proposal which does not rely on any previous comment to minimize confusion. ---------------------------------------- Feature #21550: Ractor.sharable_proc/sharable_lambda to make sharable Proc object https://bugs.ruby-lang.org/issues/21550#change-114419 * 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. (changed) Accessing outer variables are not allowed. An error is raised at the creation. 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. ### 0. Disallow accessing to the outer-variables It is simple and no confusion. ### 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. ### 3. Adding new rules (quoted from https://bugs.ruby-lang.org/issues/21550#note-7) 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 ``` For example, we can write the port example like that: ```ruby port = Ractor::Port.new Ractor.new do |;port| port << ... end ``` and it is better (shorter). 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. -- 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/