Project

General

Profile

Actions

Feature #21126

closed

Drop default_proc when Hash#freeze is called for better Ractor support

Added by osyoyu (Daisuke Aritomo) 13 days ago. Updated 12 days ago.

Status:
Rejected
Assignee:
-
Target version:
-
[ruby-core:120922]

Description

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.

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.


Related issues 3 (2 open1 closed)

Related to Ruby master - Feature #19326: Please add a better API for passing a Proc to a RactorAssignedko1 (Koichi Sasada)Actions
Related to Ruby master - Feature #18162: Shorthand method Proc#isolate to create isolated proc objectsOpenActions
Related to Ruby master - Feature #17284: Shareable ProcClosedActions

Updated by jeremyevans0 (Jeremy Evans) 13 days ago

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:

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:

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:

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.

Updated by byroot (Jean Boussier) 13 days ago

I was about to write the same thing as @jeremyevans0 (Jeremy Evans), the default_proc doesn't necessarily mutate the hash, hence dropping it on freeze isn't correct.

Actions #3

Updated by nobu (Nobuyoshi Nakada) 12 days ago

  • Related to Feature #19326: Please add a better API for passing a Proc to a Ractor added
Actions #4

Updated by nobu (Nobuyoshi Nakada) 12 days ago

  • Related to Feature #18162: Shorthand method Proc#isolate to create isolated proc objects added
Actions #5

Updated by nobu (Nobuyoshi Nakada) 12 days ago

Updated by Eregon (Benoit Daloze) 12 days ago

  • Status changed from Open to Rejected

Looks like we are on the same page here, freeze should never break semantics like this.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0