From: "Eregon (Benoit Daloze) via ruby-core" Date: 2025-08-27T20:06:56+00:00 Subject: [ruby-core:123089] [Ruby Feature#21550] Ractor.sharable_proc/sharable_lambda to make sharable Proc object Issue #21550 has been updated by Eregon (Benoit Daloze). ko1 (Koichi Sasada) wrote in #note-9: > Option 2 (allow accesses only on lexical blocks) is rejected by Matz, so I want to introduce 1 or 3. Where did matz say that? In https://bugs.ruby-lang.org/issues/21550#note-5 he said: > I'd accept confusion here (option 1) to avoid complex semantics and implementation. `I'd accept` sounds like a preference, not rejection. As I clarified in my comment after that, I see no concern about complex implementation, it's the same effort anyway: > It should be easy to implement, because if given a Proc it's the same semantics as `Ractor.new(proc_object)`, and when given a literal block it's the desired more flexible semantics which anyway both options want. And regarding complex semantics (due to different behavior with literal vs non-literal block) it's necessary to avoid breaking block semantics, which are way more fundamental than `Ractor.shareable_proc` semantics. `Kernel#lambda` already had such semantics BTW (no effect if not a literal block), and in fact for similar reasons and concerns (don't break the author of the block's intent). So there is precedent for doing exactly this kind of switching on literal block vs Proc. Option 2 seems the obvious and correct solution for the ostruct case. Though it seems that `define_method` is a frequent issue that keeps coming up with Ractor, and that's quite a specific case where we know the Proc `self` will be changed anyway. So I think `define_method(name) { @table[name] }` / `define_singleton_method(name) { @table[name] }` should just work, with and without Ractors. That would mean that `define_method` would automatically shallow-copy the environment, Ractor or not, for consistency. That's a semantic change but it seems very compatible. And `define_method` knows `self` will be the instance so no need to check if `self` is shareable. And the defined method would automatically be available to Ractors, as long as the captured variables values are shareable. If they are not, then the method can't be called on a Ractor, that's impossible anyway, same error as currently. That would mean `define_method` would just work with Ractor, vs needing pretty messy code as in your example. "if Ractor" code should be avoided as much as possible. If we are too concerned about changing `define_method` semantics we could instead make that behavior opt-in e.g. via a keyword argument to `define_method` like `define_method(name, ractor/shareable/make_shareable: true) { @table[name] }`. ---------------------------------------- Feature #21550: Ractor.sharable_proc/sharable_lambda to make sharable Proc object https://bugs.ruby-lang.org/issues/21550#change-114403 * 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. (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.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. -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/