Issue #21962 has been updated by byroot (Jean Boussier). I don't have a strong opinion here, but one argument I could see for inclusion in core is to optimize frozen constants, e.g. ```ruby SCHEMA = [ { type: :foo, tags: ["a", "b"] }, { type: :bar, tags: ["c", "d"] }, ... ].deep_freeze ``` Which is much nicer than: ```ruby SCHEMA = [ { type: :foo, tags: ["a", "b"].freeze }.freeze, { type: :bar, tags: ["c", "d"].freeze }.freeze, ... ].freeze ``` ---------------------------------------- Feature #21962: Add deep_freeze for recursive freezing https://bugs.ruby-lang.org/issues/21962#change-117077 * Author: Eregon (Benoit Daloze) * Status: Open ---------------------------------------- ### Motivation It is common to want some data structure to be immutable, e.g. to ensure it doesn't get mutated by multiple threads. See the first section of #21665 for a more detailed motivation, the summary is there is long-standing demand for it (back to #17145) for many reasons. One way/workaround to do this currently is `Ractor.make_shareable` but its name suggests a Ractor-specific purpose. The functionality of deeply freezing is separate from Ractor and it is generally useful. `Ractor.make_shareable` almost has the right semantics and has been used for years so this proposal largely follows its semantics rather than reinventing the wheel. The semantics also match the `ice_nine` gem, which further illustrates these semantics work well in practice and are expected. ### Examples It freezes deeply: ```ruby config = { db: { host: ENV["HOST"], ports: [5432] }, flags: [:a, :b] }.deep_freeze config.frozen? # => true config[:db].frozen? # => true config[:db][:host].frozen? # => true config[:db][:ports].frozen? # => true ``` Modules and classes are not frozen by `deep_freeze`: ```ruby String.deep_freeze String.frozen? # => false ``` It works for cyclic references: ```ruby a = [] a << a a.deep_freeze a.frozen? # => true ``` It calls `.freeze` on each object: ```ruby class Stats def initialize(samples) @samples = samples @mean = nil end def mean @mean ||= @samples.sum / @samples.size.to_f end def freeze mean super end end s = Stats.new([1.0, 2.0, 3.0]) s.deep_freeze s.mean # => 2.0, would raise FrozenError instead if overridden #freeze was not called ``` ### Background #21665 relaunched the discussion about `deep_freeze` and it was [discussed in a dev meeting](https://github.com/ruby/dev-meeting-log/blob/master/2025/DevMeeting-2025-11-...). @headius has said he won't have time to look at it soon. So I am proposing something concrete which hopefully can be approved as-is. ### Design * Proposed: `Kernel#deep_freeze`. Alternative: `Object.deep_freeze`. * `Kernel#deep_freeze` is consistent with `Kernel#freeze`. * `[a, b, c].deep_freeze` is nicer, more concise and more idiomatic than `Object.deep_freeze([a, b, c])`. * The proposal focuses on semantics; the exact method name and owner can be decided separately. * The return value is the receiver. * `deep_freeze` never freezes modules and classes and does not visit their internal references * Same as `Ractor.make_shareable` and `IceNine.deep_freeze` * No unexpected breakage because a class, its ancestors, their constants, etc all got frozen. * Very easy to explain in the documentation. * If people want to freeze classes/modules (rare case) they can with `.freeze` and have control and can decide whether to freeze the ancestors, constants, class variables, etc. * `deep_freeze` calls `freeze` and ensures every object is frozen * Same as `Ractor.make_shareable` and `IceNine.deep_freeze` * The already well-known hook to e.g. compute lazy caches eagerly, etc * We still check that `.freeze` did freeze the object, and raise a `RuntimeError` if not (same behavior and error message as `Ractor.make_shareable`). * Not a protocol of defining `deep_freeze` in many classes. * That does not work with circular references. * No need to invent a complex protocol when there is an existing working one (calling `.freeze`) which has proven to work well (by being used by `Ractor.make_shareable`). * Handling circular references by maintaining internally a set of visited objects to avoid walking circular references multiple times * As done in `obj_traverse_i` in `ractor.c`. * No expectation for calling `deep_freeze` repeatedly on the same object to be fast * An object flag wouldn't work for this, for the case that `freeze` raises an exception. * If some `freeze` method raises an exception, the exception is propagated. * Same as `Ractor.make_shareable` and `IceNine.deep_freeze` * Some objects visited before the exception may already be frozen, same as `Ractor.make_shareable` (unavoidable because we need to call `freeze` as we traverse the object graph). * `deep_freeze` does not stop at already-frozen objects, because they may still reference unfrozen objects. * No special treatment for shareable objects, they are frozen too. I think this addresses all the points mentioned in the [dev meeting discussion](https://github.com/ruby/dev-meeting-log/blob/master/2025/DevMeeting-2025-11-...) and in #21665 and in #17145. The traversal and freezing behavior closely follows `Ractor.make_shareable`, without introducing new semantics. ### Implementation The implementation in CRuby would be shared with `Ractor.make_shareable`, `ractor.c` already provides `rb_obj_traverse()` which is a great fit to implement this. From a high-level view the implementation works like this: `deep_freeze` recursively traverses the reachable object graph from the receiver. For each visited object: * If it is a `Class` or `Module`, it is not frozen and its internal references are not traversed. * Otherwise, Ruby calls `.freeze` on the object. * After calling `freeze`, Ruby verifies that the object is frozen. If it is not frozen, an exception is raised (same behavior and error message as `Ractor.make_shareable`). * The traversal keeps a visited set to avoid infinite recursion on cycles. ### Non-goals This proposal is focused on defining `deep_freeze` and nothing else: * This proposal does not define a new protocol requiring classes to implement custom recursive logic. * This proposal does not change `Ractor.make_shareable`. * This proposal does not freeze classes or modules. * This proposal does not attempt to define immutability as a broader language concept. ### Compatibility and risk * The proposal adds a new API and does not change existing `freeze` behavior. * It intentionally reuses existing `freeze` overrides on user objects. * It intentionally does not freeze classes/modules to avoid surprising breakage. * Its semantics are close to existing `Ractor.make_shareable`, reducing implementation risk. ### Relation to existing methods * `freeze`: freezes only the receiver (shallow). * `deep_freeze`: recursively freezes reachable objects. * `Ractor.make_shareable`: recursively freezes reachable objects except if they are already shareable. Special handling to make some objects shareable. -- https://bugs.ruby-lang.org/