[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/
Issue #21665 has been updated by schneems (Richard Schneeman). I think this is generally useful. I hit a bug in syntax_suggest that took me about 4 hours to track down that boiled down to contents of an array being shared in an unintuitive way so their mutation wasn't cleanly isolated. On the naming suggestions: I like `deep_freeze` as a name, in the US there's a concept of having a separate dedicated freezer, or at a kitchen they have walk-in freezers. These are commonly called "deep freeze" as in "put this sauce in the deep freeze." But maybe we want the API to be similar to the current `freeze` method i.e. start with "freeze" such as "freeze_all". Or possibly we introduce a module with some methods like `Freeze.all()`. We could use a different name as the concept, the core idea of freezing something is to make it immutable, to hold it in place. It could be `immutable` or another analogy like `pin` or `pin_all` (though this concept of "pinning" has connotations in other languages like Rust where it relates to memory access guarantees.) Or possibly, we could invert the problem by making the "freeze" method mean "deep freeze", and introducing a new method like "freeze_surface", "freeze_lite", "freeze_no_recurse", or "freeze_shallow". I personally think it's a little surprising for someone to learn that calling "freeze" does not actually make the object immutable. This approach would take longer, and require a deprecation process. So maybe it's not the best short-term fix, but I think it's worth keeping in mind for the longer term. Perhaps we introduce an explicit "deep" api and an explicit "shallow" api now and that would make it easier to deprecate or change plain "freeze" behavior in the future. ---------------------------------------- Feature #21665: Revisit Object#deep_freeze to support non-Ractor use cases https://bugs.ruby-lang.org/issues/21665#change-115059 * 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/
Issue #21665 has been updated by headius (Charles Nutter).
the core idea of freezing something is to make it immutable, to hold it in place. It could be immutable or another analogy
The `immutable` name is an interesting concept but maybe more in the domain of #18035 than `deep_freeze`. One benefit `immutable` would have is that it could potentially be used to return a *new* collection that's optimized for immutability. ```rub # a normal mutable array instance ary = [1,2,3] # an immutable array that's packed and optimized imut = ary.immutable ``` In any case, though, `immutable` seems like something that would have to be there from birth, so I don't know how well it applies to freezing (making an object immutable long after birth).
Or possibly, we could invert the problem by making the "freeze" method mean "deep freeze"
I like the idea of making `freeze` actually deep freeze reachable objects... but that's probably too big a leap for even Ruby 4.0. I don't have a strong preference between any of the proposed names. Everyone immediately knows what `deep_freeze` is (in the English-speaking "west" anyway) so that's still my preference, and I'm leaning heavily toward it being a class method so it can't be overridden or replaced under normal circumstances. On that note, I really don't like `make_shareable` because of "make" is so ambiguous as a verb. Is it producing some new shareable object (as in "make into a shareable")? No, it's actually modifying the given object ("make it be shareable"). Perhaps this is too off topic, but `make_shareable` would probably be better as `share` as in `Ractor.share(obj)`. That implies it's transitioning the object into some private, unshareable format into a shareable format so it can be shared. Similarly, the `freeze` prefixed names might feel better to some folks as a strong verb form for the first part of the name. Other ideas expanding on the ideas of immutability, active verbs, and altering the objects in place... * `Object.seal(obj)` as in sealing it so no further changes can be made * `Object.finish(obj)` as in finishing the object after it has finished mutation * `Object.harden(obj)` as in hardening the object so it can't be changed * `Object.crystallize(obj)` because this is Ruby after all Actually, I kind of like `seal` because it doesn't need to imply that things like classes would be frozen and it has some precedent in other languages (Java libraries can declare that a namespace is "sealed" which means no further classes can be loaded in that namespace.) But `deep_freeze` still probably wins on recognizability. ---------------------------------------- Feature #21665: Revisit Object#deep_freeze to support non-Ractor use cases https://bugs.ruby-lang.org/issues/21665#change-115060 * 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/
Issue #21665 has been updated by Eregon (Benoit Daloze). +1 from me. We see [a lot of](https://github.com/search?q=Ractor.make_shareable+language%3ARuby&type=code&l=Ruby): ```ruby CONST = [...] Ractor.make_shareable(CONST) if defined?(Ractor) ``` in gems, but that should really be: ```ruby CONST = [...].deep_freeze ``` The intent is clearer (want to avoid mutations to that object and the objects it refers to) and it's cleaner as this is not Ractor-specific. --- Name-wise I think `deep_freeze` is the obvious one and makes the most sense. I think it's the only name that when said every Rubyist will understand what it does, so why go for something less clear? Regarding freezing modules/classes: I believe nobody wants those semantics, at least by default, otherwise `Foo.new.deep_freeze` should then freeze that instance, the `Foo` class, `Object` the ancestor of `Foo`, all values of constants of Object, i.e. everything reachable through constants. That's clearly unreasonable. If one wants that behavior, they can implement it with `ObjectSpace.reachable_objects_from`, but it's extremely rare to want that, as e.g. it prevents adding methods to classes later and is unrelated to making an objects and the objects it refers to frozen so they can't be mutated accidentally. A practical example is: ```ruby CONVERT = { Integer => [-> { ... }, -> { ... }], Array => -> [-> { ... }, -> { ... }], Hash => -> [-> { ... }, -> { ... }], }.deep_freeze ``` This is clear, the user does not want to freeze the Integer class, they want to avoid the Hash instances and Array instances to be mutated. ---------------------------------------- Feature #21665: Revisit Object#deep_freeze to support non-Ractor use cases https://bugs.ruby-lang.org/issues/21665#change-115068 * 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/
Issue #21665 has been updated by pabloh (Pablo Herrero). I really think the naming scheme should distinguish classes/modules from regular objects. Freezing modules/classes should be separated into their own ad hoc methods. Since you can break a lot of existing code if you freeze the wrong class by accident. Also, it's still very unclear to me how it should handle special cases, like singleton classes or other metaobjects. So this code: ``` CONST = [...] Ractor.make_shareable(CONST) if defined?(Ractor) ``` Could be something like (and keep the same semantics): ``` CONST = Object.freeze_objects([...]) # Won't touch classes/modules ``` And then choose some other name for metaobjects: - `Module.freeze_module(mod)` # Only freeze a a single metaobject - `Object.freeze_objects([...], modules: true)` # Freeze every objects including metaobjects - `Object.freeze_objects([...], metaobjects: true)` # Same Finally, if you can have some code like this: ``` obj = Object.new def obj.foo()= "FOOOO" obj.class == Object # True, but singleton class changed Module.freeze_module(obj.class) # What should happen here??? ``` `obj.class` will still return a reference to the `Object` class, but the user may have intended to freeze the singleton class. Perhaps for some other people the intent is more obvious, but for me it's still a bit confusing, and I don't have a good answer to this question. ---------------------------------------- Feature #21665: Revisit Object#deep_freeze to support non-Ractor use cases https://bugs.ruby-lang.org/issues/21665#change-115094 * 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/
Issue #21665 has been updated by retro (Josef Ε imΓ‘nek). Was it considered keep `make_shareable` name, but port it outside of `Ractor` class to make it independent of the "sharing" implementation and useful for JRuby and others? ---------------------------------------- Feature #21665: Revisit Object#deep_freeze to support non-Ractor use cases https://bugs.ruby-lang.org/issues/21665#change-115098 * 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/
Issue #21665 has been updated by Eregon (Benoit Daloze). This came up again in https://github.com/dry-rb/dry-struct/pull/203. People are thinking to use `Ractor.make_shareable` "because it's core and fast" instead of e.g. the IceNine gem. But `Ractor.make_shareable` is only available on CRuby currently, because Ractor is a feature that makes mostly sense for CRuby but not much for other Rubies without a GVL. And it's very long, verbose and unclear for the very common usage of "make this data structure deeply frozen". I'll note the `ice_nine` gem does not freeze modules/classes either, it seems everyone already expects that. IIRC this was discussed in a previous dev meeting, what was the result? @headius Do you recall? What's missing here? A PR to implement it? ---------------------------------------- Feature #21665: Revisit Object#deep_freeze to support non-Ractor use cases https://bugs.ruby-lang.org/issues/21665#change-116590 * 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/
Issue #21665 has been updated by ufuk (Ufuk Kayserilioglu). # It was discussed in the November 2025 meeting and it seems the conclusion was that @headius would be bringing a proposal to address @matz's concerns: https://github.com/ruby/dev-meeting-log/blob/master/2025/DevMeeting-2025-11-... ---------------------------------------- Feature #21665: Revisit Object#deep_freeze to support non-Ractor use cases https://bugs.ruby-lang.org/issues/21665#change-116591 * 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/
participants (6)
-
Eregon (Benoit Daloze) -
headius (Charles Nutter) -
pabloh (Pablo Herrero) -
retro -
schneems (Richard Schneeman) -
ufuk (Ufuk Kayserilioglu)