52

In Ruby, there is Object#freeze, which prevents further modifications to the object:

class Kingdom
  attr_accessor :weather_conditions
end

arendelle = Kingdom.new
arendelle.frozen? # => false
arendelle.weather_conditions = 'in deep, deep, deep, deep snow'
arendelle.freeze
arendelle.frozen? # => true
arendelle.weather_conditions = 'sun is shining'
  # !> RuntimeError: can't modify frozen Kingdom

script = 'Do you want to build a snowman?'.freeze
script[/snowman/] = 'castle of ice'
  # !> RuntimeError: can't modify frozen String

However, there is no Object#unfreeze. Is there a way to unfreeze a frozen kingdom?

4 Answers 4

60

Update: As of Ruby 2.7 this no longer works!


Yes and no. There isn't any direct way using the standard API. However, with some understanding of what #freeze? does, you can work around it. Note: everything here is implementation details of MRI's current version and might be subject to change.


Objects in CRuby are stored in a struct RVALUE.
Conveniently, the very first thing in the struct is VALUE flags;.
All Object#freeze does is set a flag, called FL_FREEZE, which is actually equal to RUBY_FL_FREEZE. RUBY_FL_FREEZE will basically be the 11th bit in the flags.
All you have to do to unfreeze the object is unset the 11th bit.

To do that, you could use Fiddle, which is part of the standard library and lets you tinker with the language on C level:

require 'fiddle'

class Object
  def unfreeze
    Fiddle::Pointer.new(object_id * 2)[1] &= ~(1 << 3)
  end
end

Non-immediate value objects in Ruby are stored on address = their object_id * 2. Note that it's important to make the distinction so you would be aware that this wont let you unfreeze Fixnums for example.

Since we want to change the 11th bit, we have to work with the 3th bit of the second byte. Hence we access the second byte with [1].

~(1 << 3) shifts 1 three positions and then inverts the result. This way the only bit which is zero in the mask will be the third one and all other will be ones.

Finally, we just apply the mask with bitwise and (&=).


foo = 'A frozen string'.freeze
foo.frozen? # => true
foo.unfreeze
foo.frozen? # => false
foo[/ (?=frozen)/] = 'n un'
foo # => 'An unfrozen string'
14
  • 9
    Messing around with Ruby's internals is risky. You should address that in your answer.
    – Stefan
    Commented Feb 25, 2016 at 17:54
  • 8
    @Stefan, I mentioned it in the opening paragraph. I consider it to be self evident.
    – ndnenkov
    Commented Feb 25, 2016 at 17:56
  • 1
    @shevy well, it depends. It might be perfectly fine to unfreeze the frozen kingdom from the example. But there are objects that don't like to be unfrozen. For example, :foo.unfreeze crashes Ruby with a segmentation fault.
    – Stefan
    Commented Feb 23, 2017 at 8:32
  • 1
    @ndn you're right, I've totally overlooked that. But does a symbol have immediate value? Nevertheless, there could be situations where the frozen state of an object is taken for granted and changing it would lead to unexpected behavior.
    – Stefan
    Commented Feb 23, 2017 at 8:59
  • 1
    @Stefan, according to the documentation, immediate values are used for nil, true, false, Fixnums, Symbols, and some Floats
    – ndnenkov
    Commented Feb 23, 2017 at 9:06
40

No, according to the documentation for Object#freeze:

There is no way to unfreeze a frozen object.

The frozen state is stored within the object. Calling freeze sets the frozen state and thereby prevents further modification. This includes modifications to the object's frozen state.

Regarding your example, you could assign a new string instead:

script = 'Do you want to build a snowman?'
script.freeze

script = script.dup if script.frozen?
script[/snowman/] = 'castle of ice'
script #=> "Do you want to build a castle of ice?"

Ruby 2.3 introduced String#+@, so you can write +str instead of str.dup if str.frozen?

2
  • 5
    I especially like your addition mentioning the String#+@ part, since this is fairly new, it appears to be a good use to have mentioned it. I remember I noticed nobu doing some changes there, via the changelog back then, and I did not know about +str.
    – shevy
    Commented Feb 8, 2017 at 8:47
  • 1
    The un-freezability is especially important with Ractor in Ruby 3.0. Commented Dec 17, 2020 at 5:12
5
frozen_object = %w[hello world].freeze
frozen_object.concat(['and universe']) # FrozenError (can't modify frozen Array)
frozen_object.dup.concat(['and universe']) # ['hello', 'world', 'and universe']
3
  • 2
    This doesn't modify the object, it modifies it's copy.
    – ndnenkov
    Commented Jan 7, 2019 at 7:39
  • Because of you shouldn't "unfreeze" frozen objects in Ruby, it's supposed the purpose of this functionality is basically keep your objects immutable. Even Rubocop, time ago introduced a rule where every single .rb should have "The magic Ruby comment" and constants should always have a frozen state. Ruby 2.6 already comes with this feature by default. Commented Jan 7, 2019 at 10:00
  • I believe this is the correct answer. Duplicate the variable and modify it as you see fit.
    – metrix
    Commented Mar 29, 2021 at 23:55
1

As noted above copying the variable back into itself also effectively unfreezes the variable.

As noted this can be done using the .dup method:

var1 = var1.dup

This can also be achieved using:

var1 = Marshal.load(Marshal.dump(var1))

I have been using Marshal.load(Marshal.dump( ... )

I have not used .dup and only learned about it through this post.

I do not know what if any differences there are between Marshal.load(Marshal.dump( ... )

If they do the same thing or .dup is more powerful, then stylistically I like .dup better. .dup states what to do -- copy this thing, but it does not say how to do it, whereas Marshal.load(Marshal.dump( ... ) is not only excessively verbose, but states how to do the duplication -- I am not a fan of specifying the HOW part if the HOW part is irrelevant to me. I want to duplicate the value of the variable, I do not care how.

1
  • As noted above, this doesn't modify the object, it modifies a copy.
    – Adrian
    Commented Jun 27, 2021 at 17:57

Not the answer you're looking for? Browse other questions tagged or ask your own question.