Feature #21550
openRactor.sharable_proc/sharable_lambda to make sharable Proc object
Description
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:
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:
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.
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).
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 (Benoit Daloze) 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:
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.
# (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.
Updated by ko1 (Koichi Sasada) about 21 hours ago
- Related to Feature #21039: Ractor.make_shareable breaks block semantics (seeing updated captured variables) of existing blocks added
Updated by matz (Yukihiro Matsumoto) about 12 hours ago
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.