[ruby-core:120922] [Ruby master Feature#21126] Drop default_proc when Hash#freeze is called for better Ractor support

Issue #21126 has been reported by osyoyu (Daisuke Aritomo). ---------------------------------------- Feature #21126: Drop default_proc when Hash#freeze is called for better Ractor support https://bugs.ruby-lang.org/issues/21126 * Author: osyoyu (Daisuke Aritomo) * Status: Open ---------------------------------------- Hash instances with default_proc set cannot be sent/moved across Ractors, even if they are frozen. Consider the following code. Using a default proc to set an empty Array is a very common pattern, even introduced in the docs. ```ruby h = Hash.new {|h, k| h[k] = [] } h[:foo] << 1 h.freeze Ractor.new(h) {|h| p h }.take # <internal:ractor>:282:in 'Ractor.new': allocator undefined for Proc (TypeError) ``` https://docs.ruby-lang.org/en/3.4/Hash.html#class-Hash-label-Default+Proc One must explicitly call `h.default_proc = nil` before sending the hash to another Ractor. This isn't the most friendly way for the programmer since (1) it is not easy to spot that the default hash is rendering the Hash unsendable, and (2) `Hash#default_proc=` isn't a widely known API anyway (at least from my perspective). ## Proposal Automatically drop the `default` `default_proc` when Hash#freeze is called. They have little use after the Hash gets frozen. Nevertheless, it should be pointed out that this is an incompatibility for `Hash#default` `Hash#default_proc` -- they currently return the original value, but they will return `nil`. Patch: https://github.com/ruby/ruby/pull/12717 Dropping the default_proc on `Hash#freeze` will also be nicer for `Ractor.make_shareable` users, since it does not require users to find the particular Hash with the default_proc buried somewhere. -- https://bugs.ruby-lang.org/

Issue #21126 has been updated by jeremyevans0 (Jeremy Evans). I think this is a bad idea. It makes `freeze` change the hash in a non-backwards compatible way. For `Hash#default`, I think it makes no sense at all: ```ruby h = Hash.new(0) h[1] # 0 h.freeze h[1] # Before: 0, After: nil ``` What is the explanation for dropping `default` in this case? It shouldn't even make the hash ractor unsharable, as long as the default value is ractor sharable. For `Hash#default_proc`, in the case where the proc does not modify the hash, the issue is the same. It's possible to use a ractor sharable default proc: ```ruby pr = Ractor.make_shareable(Object.class_eval{proc{0}}) h = Hash.new(&pr) h[1] # 0 h.freeze h[1] # Before: 0, After: nil ``` For the case where the default proc is modifying the hash, triggering the default proc should result in a FrozenError: ```ruby h = Hash.new{|h,k| h[k] = 0} h[1] # 0 h.freeze h[1] # 0 h[2] # Before: FrozenError, After: nil ``` I expect this proposal would break a substantial number of Ruby libraries and applications. I think we should not break backwards compatibility to work around Ractor limitations. ---------------------------------------- Feature #21126: Drop default_proc when Hash#freeze is called for better Ractor support https://bugs.ruby-lang.org/issues/21126#change-111808 * Author: osyoyu (Daisuke Aritomo) * Status: Open ---------------------------------------- Hash instances with default_proc set cannot be sent/moved across Ractors, even if they are frozen. Consider the following code. Using a default proc to set an empty Array is a very common pattern, even introduced in the docs. ```ruby h = Hash.new {|h, k| h[k] = [] } h[:foo] << 1 h.freeze Ractor.new(h) {|h| p h }.take # <internal:ractor>:282:in 'Ractor.new': allocator undefined for Proc (TypeError) ``` https://docs.ruby-lang.org/en/3.4/Hash.html#class-Hash-label-Default+Proc One must explicitly call `h.default_proc = nil` before sending the hash to another Ractor. This isn't the most friendly way for the programmer since (1) it is not easy to spot that the default hash is rendering the Hash unsendable, and (2) `Hash#default_proc=` isn't a widely known API anyway (at least from my perspective). ## Proposal Automatically drop the `default` `default_proc` when Hash#freeze is called. They have little use after the Hash gets frozen. Nevertheless, it should be pointed out that this is an incompatibility for `Hash#default` `Hash#default_proc` -- they currently return the original value, but they will return `nil`. Patch: https://github.com/ruby/ruby/pull/12717 Dropping the default_proc on `Hash#freeze` will also be nicer for `Ractor.make_shareable` users, since it does not require users to find the particular Hash with the default_proc buried somewhere. -- https://bugs.ruby-lang.org/

Issue #21126 has been updated by byroot (Jean Boussier). I was about to write the same thing as @jeremyevans0, the `default_proc` doesn't necessarily mutate the hash, hence dropping it on freeze isn't correct. ---------------------------------------- Feature #21126: Drop default_proc when Hash#freeze is called for better Ractor support https://bugs.ruby-lang.org/issues/21126#change-111809 * Author: osyoyu (Daisuke Aritomo) * Status: Open ---------------------------------------- Hash instances with default_proc set cannot be sent/moved across Ractors, even if they are frozen. Consider the following code. Using a default proc to set an empty Array is a very common pattern, even introduced in the docs. ```ruby h = Hash.new {|h, k| h[k] = [] } h[:foo] << 1 h.freeze Ractor.new(h) {|h| p h }.take # <internal:ractor>:282:in 'Ractor.new': allocator undefined for Proc (TypeError) ``` https://docs.ruby-lang.org/en/3.4/Hash.html#class-Hash-label-Default+Proc One must explicitly call `h.default_proc = nil` before sending the hash to another Ractor. This isn't the most friendly way for the programmer since (1) it is not easy to spot that the default hash is rendering the Hash unsendable, and (2) `Hash#default_proc=` isn't a widely known API anyway (at least from my perspective). ## Proposal Automatically drop the `default` `default_proc` when Hash#freeze is called. They have little use after the Hash gets frozen. Nevertheless, it should be pointed out that this is an incompatibility for `Hash#default` `Hash#default_proc` -- they currently return the original value, but they will return `nil`. Patch: https://github.com/ruby/ruby/pull/12717 Dropping the default_proc on `Hash#freeze` will also be nicer for `Ractor.make_shareable` users, since it does not require users to find the particular Hash with the default_proc buried somewhere. -- https://bugs.ruby-lang.org/

Issue #21126 has been updated by Eregon (Benoit Daloze). Status changed from Open to Rejected Looks like we are on the same page here, `freeze` should never break semantics like this. ---------------------------------------- Feature #21126: Drop default_proc when Hash#freeze is called for better Ractor support https://bugs.ruby-lang.org/issues/21126#change-111822 * Author: osyoyu (Daisuke Aritomo) * Status: Rejected ---------------------------------------- Hash instances with default_proc set cannot be sent/moved across Ractors, even if they are frozen. Consider the following code. Using a default proc to set an empty Array is a very common pattern, even introduced in the docs. ```ruby h = Hash.new {|h, k| h[k] = [] } h[:foo] << 1 h.freeze Ractor.new(h) {|h| p h }.take # <internal:ractor>:282:in 'Ractor.new': allocator undefined for Proc (TypeError) ``` https://docs.ruby-lang.org/en/3.4/Hash.html#class-Hash-label-Default+Proc One must explicitly call `h.default_proc = nil` before sending the hash to another Ractor. This isn't the most friendly way for the programmer since (1) it is not easy to spot that the default hash is rendering the Hash unsendable, and (2) `Hash#default_proc=` isn't a widely known API anyway (at least from my perspective). ## Proposal Automatically drop the `default` `default_proc` when Hash#freeze is called. They have little use after the Hash gets frozen. Nevertheless, it should be pointed out that this is an incompatibility for `Hash#default` `Hash#default_proc` -- they currently return the original value, but they will return `nil`. Patch: https://github.com/ruby/ruby/pull/12717 Dropping the default_proc on `Hash#freeze` will also be nicer for `Ractor.make_shareable` users, since it does not require users to find the particular Hash with the default_proc buried somewhere. -- https://bugs.ruby-lang.org/
participants (4)
-
byroot (Jean Boussier)
-
Eregon (Benoit Daloze)
-
jeremyevans0 (Jeremy Evans)
-
osyoyu (Daisuke Aritomo)