[ruby-core:123042] [Ruby Feature#21550] Ractor.sharable_proc/sharable_lambda to make sharable Proc object

Issue #21550 has been reported by ko1 (Koichi Sasada). ---------------------------------------- Feature #21550: Ractor.sharable_proc/sharable_lambda to make sharable Proc object https://bugs.ruby-lang.org/issues/21550 * 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. ``` 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 @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/

Issue #21550 has been updated by matz (Yukihiro Matsumoto). I understand the concern for future confusion, but it's a trade-off. I'd accept confusion here (option 1) to avoid complex semantics and implementation. Matz. ---------------------------------------- Feature #21550: Ractor.sharable_proc/sharable_lambda to make sharable Proc object https://bugs.ruby-lang.org/issues/21550#change-114368 * 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/
participants (2)
-
ko1 (Koichi Sasada)
-
matz (Yukihiro Matsumoto)