Project

General

Profile

Feature #21550

Updated by ko1 (Koichi Sasada) 1 day ago

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.  

Back