Feature #21550
Updated by Eregon (Benoit Daloze) 2 months 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 = (shareable_proc) (sharable_proc)
worker << task
task = (shareable_proc) (sharable_proc)
worker << task
task = (shareable_proc) (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.shareable_proc{ 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.