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.