From: "headius (Charles Nutter) via ruby-core" Date: 2025-11-04T00:13:53+00:00 Subject: [ruby-core:123672] [Ruby Feature#21665] Revisit Object#deep_freeze to support non-Ractor use cases Issue #21665 has been reported by headius (Charles Nutter). ---------------------------------------- Feature #21665: Revisit Object#deep_freeze to support non-Ractor use cases https://bugs.ruby-lang.org/issues/21665 * Author: headius (Charles Nutter) * Status: Open ---------------------------------------- ## Proposal: Introduce `Object#deep_freeze` (or similar name) to freeze an entire object graph I would like to re-propose the addition of Object#deep_freeze as a way to explicitly freeze an entire object graph. This proposal was rejected some years ago after being brought up in https://bugs.ruby-lang.org/issues/17145. The proposal was rejected in favor of making Ractor-specific methods like Ractor.make_shareable. There are a number of reasons why I believe `deep_freeze` is still an important addition: * Rubyists have been requesting a way to deep freeze an object graph for many years (decades?), far longer than Ractor has existed. * Immutable objects are the safest way to safe concurrency, with or without parallel threading or Ractor. * In fact, deep freezing has utility *completely unrelated to concurrency*, such as to guarantee that a large graph of objects will not be modified in the future. * In the absence of `deep_freeze`, users have been forced to implement the behavior themselves, rely on third-party libraries, or call `Ractor.make_shareable` even if they never intend to use Ractor. * The existing `Ractor.make_shareable` primarily does a deep freeze internally. Given the steady move toward making immutability the norm in Ruby, it seems clear to me that deep freezing is a feature that is long overdue. ## Revisiting arguments for rejecting `deep_freeze`: A number of reasons were given in #17145 for preferring the `Ractor.make_shareable` method and rejecting `deep_freeze`. I address those here: @ko1: > One concern about the name "freeze" is, what happens on shareable objects on Ractors. > For example, Ractor objects are shareable and they don't need to freeze to send beyond Ractor boundary. As mentioned above, deep freezing has utility completely separate from Ractors and concurrency. It is a frequently-requested and very useful feature to add. I think we should treat this as a standalone feature, and treat enhancements for Ractors as a separate concern. @ko1: > I also want to introduce Mutable but shareable objects using STM (or something similar) writing protocol (shareable Hash). What happens on deep_freeze? Five years later, I believe this has not yet happened. A potential future optimization for Ractor should not be justification to reject a useful feature today. If users implement their code using primarily immutable objects now, it's unlikely that they will want those same objects to be mutable in the future (this applies to deep freezing as well as `make_shareable`). @eregon: > A dynamic call to freeze causes extra calls, and needs checks that it was indeed frozen. > So for efficiency I think it would be better to mark as frozen internally without a call to freeze on every value. I agree with the concerns about dynamic calls to freeze and overridden versions of the method. It may make more sense to implement this as a utility method, like `Object.deep_freeze(obj)` (a non-overridable class utility method). This is essentially what has been implemented within `Ractor.make_shareable` today. @ko1: > Maybe the author don't want to care about Ractor. > The author want to declare "I don't touch it". So "deep_freeze" is better. This was actually given as a justification for a `deep_freeze` method versus something like `Object#to_shareable`, and yet what we ended up with was a method that requires users know about Ractor. I believe there should be a `deep_freeze` method that has nothing to do with Ractor. And users on JRuby and TruffleRuby already can get full parallelism today without Ractor. They do not care about Ractor, but they definitely care about deep freezing. @eregon: > I don't like anything with "ractor" in the name, that becomes not descriptive of what it does and IMHO looks weird for e.g. gems not specifically caring about Ractor. This is a large part of my justification for revisiting this proposal. Users should not have to care about or want to use Ractor just so they can deep freeze an object graph, because it has utility far beyond Ractor. @ko1: > I implemented Object#deep_freeze(skip_shareable: false) for trial. > https://github.com/ko1/ruby/pull/new/deep_freeze There's already a prototype of this, though I suspect this logic essentially became `Ractor.make_shareable` in the end. I believe it would be acceptable to implement `Ractor.make_shareable` by calling `deep_freeze` since there's largely no difference in visible behavior (other than Ractor-specific optimizations like marking a whole graph as shareable). @eregon: > How about first having deep_freeze that just freezes everything (except an object's class)? This is a good proposal. I believe it is what 99% of users currently calling `make_shareable` actually want, and again there's utility well beyond Ractor and concurrency scenarios. @eregon: > So we could mark as deeply frozen first, and remember to undo that if we cannot freeze some object. > However, is there any object that cannot be frozen? I would think not. The majority of uses of `make_shareable` I have seen are called exactly once on a graph of objects. It does not seem to be typical to repeatedly call `make_shareable`. I understand the desire to have a `shareable` bit for Ractor optimization, but that is a *separate feature* from deep freezing an object graph. There are many cases where we will only call `deep_freeze` once to ensure a graph is fully frozen before publishing it for other code to see, and most of these cases will not try to re-deep-freeze that graph. Ractor's need to "double-check" shareability is orthogonal to the discussion about deep freezing and should not be justification for rejecting `deep_freeze`. @eregon brought up concerns about not calling the custom `freeze` method on user types, since they may want to eagerly cache some data. I believe that discussion is out of scope. `deep_freeze` would be defined to only free the objects that are directly walkable from a root object, and only setting frozen bits. A new overridable method could be introduced that `deep_freeze` would call if present, but otherwise it should just do fast-path object freeze flag setting. @marcandre: > Looking at def freeze in the top ~400 gems, I found 64 in sequel gem alone, and 28 definitions in the rest ����. This comment provides a breakdown of custom `freeze` methods and the reasons they are implemented. Again, I believe this is out of scope for the discussion at hand. Forcing objects to "prepare for deep freezing" is a separate consideration that will be very library-specific, since every library may want to prepare in a different way. But they *all* want the ability to recursively mark objects as frozen, which is a runtime-level feature. @ko1: > We discussed about the name "deep_freeze", and Matz said deep_freeze should be only for freezing, not related to Ractor. So classes/module should be frozen if [C].deep_freeze. This is why I proposed a Object#deep_freeze(skip_shareable: true) and Ractor.make_shareable(obj). Avoiding classes and modules when deep freezing seems like a reasonable option to me. Naming could make this behavior clear, but again I believe 99% of users just want a plain old object `deep_freeze`. And this is again conflating two separate concerns: * deep freezing * marking an entire graph as shareable These are ��� and should be ��� two separate features. The deep freezing feature should not depend on setting shareability bits, since shareability is only meaningful in the context of Ractors. @ko1: > So naming issue is reamained? > > Object#deep_freeze (matz doesn't like it) > Object#deep_freeze(skip_sharable: true) (I don't know how Matz feel. And it is difficult to define Class/Module/... on skip_sharable: false) > Ractor.make_shareable(obj) (clear for me, but it is a bit long) > Ractor.shareable!(obj) (shorter. is it clear?) > Object#shareable! (is it acceptable?) > ... other ideas? I outline some alternatives below. ## Alternative forms: @matz didn't like `deep_freeze` five years ago. How do you feel about it now, @matz? Some alternatives with justification: * Object.deep_freeze(obj) This would make sense to avoid users being able to override the `deep_freeze` behavior, and would make it feel more like a global utility method with special behavior. * Object#freeze(obj, deep: true) * Object#freeze(obj, recursive: true) These work within the existing `freeze` method and still convey intent, but may break APIs that don't expect to receive keyword arguments. And there are some alternative names, which may work as either instance methods or class methods: * `freeze_recursive` * `freeze_all` * `freeze!` * `freeze_reachable_objects` (long but a variation of this might address concerns about not freezing classes and modules) -- 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/