From: "Eregon (Benoit Daloze) via ruby-core" Date: 2025-08-27T20:52:28+00:00 Subject: [ruby-core:123090] [Ruby Feature#21550] Ractor.sharable_proc/sharable_lambda to make sharable Proc object Issue #21550 has been updated by Eregon (Benoit Daloze). Option 3 doesn't seem good because it would break the block if the block is run on Ruby 3.4 and before, as the value of `a` would be `nil`. One would need to have 2 copies of the block which is clearly inconvenient. I think Ractor should be able to use captured variables, because this is one of the most elegant ways to pass data/input in Ruby. But it should be safe, and that means not breaking normal Ruby block semantics (at least for blocks which don't explicitly opt-in to the environment copy behavior). So I think Option 4 would still be the best, and that's the option proposed in #21039: * `Ractor.sharable_proc` is the same for literal and non-literal blocks * `Ractor.sharable_proc` raises if it captures a variable which is reassigned inside or after the block/Proc given to `Ractor.sharable_proc`. Notice we already agree on "`Ractor.sharable_proc` raises if it captures a variable which is reassigned inside the block/Proc given to `Ractor.sharable_proc`". So it's just adding `or after`, which makes it fully safe and compatible with regular block semantics. I don't think that check is very difficult to implement, in fact I can help implementing it. Ruby code to make it clear: ```ruby # OK because does not change the semantics of b def example a = 1 a += 2 b = proc { a } Ractor.sharable_proc(&b) end # OK because does not change the semantics of the block def example a = 1 a += 2 Ractor.sharable_proc { a } end # error (that everyone seems to agree on) def example a = 1 b = proc { a = 2 } Ractor.sharable_proc(&b) end # error (that everyone seems to agree on) def example a = 1 Ractor.sharable_proc { a = 2 } end # error (the case we discussing about): the code clearly assumes it can reassigns `a` but the `sharable_proc` would not respect it, i.e. `sharable_proc` would break Ruby block semantics # Also note the Ractor.sharable_proc call might be far away from the block, so one can't tell when looking at the block that it would be broken by `sharable_proc` (if no error for this case) def example a = 1 b = proc { a } Ractor.sharable_proc(&proc { a }) a = 2 end # I think should be error too, semantics are ill-defined in such a case, the code clearly assumes it can reassigns `a` but the `sharable_proc` would not respect it def example a = 1 Ractor.sharable_proc { a } a = 2 end ``` That check can be more strict for convenience (e.g. raise if the assignments are not trivially all before the block/Proc), I think that's fine because it's fairly rare to reassign a variable after a block captures it, so won't be a practical limitation anyway. But still, such cases should still behave correctly according to block semantics, hence should be forbidden for `Ractor.sharable_proc`. Re `eval` and `binding` it's so rare and such a corner case in combination with reassigning a variable after a block captures it that I think it is acceptable to expose that `Ractor.sharable_proc` copies the environment for those extremely rare cases. BTW, Proc#binding is already not supported for a `sharable_proc`: ``` $ ruby -e 'nil.instance_exec { a = 1; b = proc { a }; b2 = Ractor.make_shareable(b); p b2.binding }' -e:1:in `binding': Can't create Binding from isolated Proc (ArgumentError) ``` So `binding`/`eval` is in general already not fully respected with Ractor anyway. Some edge cases for clarity: ```ruby # OK, we cannot detect it, extreme corner case unlikely to appear in any real code. The sharable_proc will capture 1, b will capture 2. def example a = 0 b = proc { a } p = Ractor.sharable_proc(&proc { a }) eval("a = 2") # or binding.local_variable_set(:a, 2), or b.binding.local_variable_set(:a, 2) [b, p] end # error, `a` is reassigned after the block def example a = 0 while condition b = proc { a } Ractor.sharable_proc(&proc { a }) a += 1 end end # error, `a` might be reassigned (if condition is true twice or more, but we have to analyze statically so be conservative) def example while condition a = rand b = proc { a } Ractor.sharable_proc(&proc { a }) end end ``` As said before, I'm OK with option 2, but it's less flexible than option 4. We could also have a mix and allow everything for literal block, and option 4 for Proc. I remain strongly against option 1, I think it is a language design mistake we won't be able to fix later. And for `define_method` I think we should have something more convenient than `Ractor.shareable_proc` (see previous comment). ---------------------------------------- Feature #21550: Ractor.sharable_proc/sharable_lambda to make sharable Proc object https://bugs.ruby-lang.org/issues/21550#change-114404 * 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/