Words and Code

One writer’s journey from words to code.

Bundle Up & Let Your Objects Do the Freezing: Frozen Hashes

#technicaltuesdays, rails, ruby

Snowmageddon. Snowpocalypse. The Deep Freeze of 2015. You can call it whatever you want, but we can all agree on one thing: it’s so damn cold outside. In fact, it’s so cold that I recently saw this error message while debugging a Postgres issue: RuntimeError: can't modify frozen Hash

My first thought was, naturally, holy shit, even my hashes are frozen. I did a little digging, and it turns out that we aren’t the only ones susceptible to these chilly temperatures. Apparently, objects can also freeze!

So, in honor of the frozen tundra that we all seem to currently inhabit, I decided to explore the icy-cold depths of Rails frozen objects. Before you read on though, you should know: I’m going to use only gifs from the Disney animated film, Frozen — don’t say I didn’t warn you.

Objects As Cold As Ice

First things first: what’s a frozen object? Well, they’re frozen, which means they can’t change – they’re immutable and cannot be modified. There are some objects in Ruby that are perpetually frozen, such as any Fixnum, Bignum, Float, and Symbol objects.


You’ve probably worked with all of these objects before, but may not know that they’re actually frozen! Don’t believe me? Well, you can check for yourself. Just call the frozen? method on any object of this class:

1
2
3
4
5
6
7
8
9
$ irb
2.2.0 :001 > 1.frozen?
 => true
2.2.0 :002 > a = :im_a_symbol
 => :im_a_symbol
2.2.0 :003 > a.frozen?
 => true
2.2.0 :004 > 3.14.frozen?
 => true

This kind of makes sense though, right? Imagine if I could just start rewriting values all over the damn place. I could set 1 equal to nil and make 2 equal 3, and then we’d really hit crazytown.

Frozen objects keep certain things secure and certain in Ruby (and also in Rails!) So, freezing objects is a…good thing? Well, yes. But also, no.

The Freeze Method: A Cold Piece Of Work

Just as there’s a method to check whether an object is frozen? or not, there’s also a method to freeze mutable objects and make them immutable called – you guessed it – freeze. This method is pretty simple: yuo just call it on an object in order to rpevent further modifications to said object. If you do try to modify the object after freezing it, you’ll get a RuntimeError:

1
2
3
4
5
6
7
8
2.2.0 :005 > x = {1 => 'a', 2 => 'b', 3 => 'c'}
 => {1=>"a", 2=>"b", 3=>"c"}
2.2.0 :006 > x.freeze
 => {1=>"a", 2=>"b", 3=>"c"}
2.2.0 :007 > x.frozen?
 => true
2.2.0 :008 > x[1] = 'b'
RuntimeError: can't modify frozen Hash

But before you get all Elsa on me and start freezing everything, you should know some things about how to use the freeze method as a responsible developer:

  1. First and foremost: don’t try to call a defrost or thaw method, because there’s no such thing. In fact, there is absolutely no way to unfreeze an object. It simply cannot be undone. So, proceed with great caution when freezing things. Or maybe just don’t do it at all.

  2. The freeze method operates on an object reference, not on a variable. Wait, whut? If this sounds like Ruby objects aren’t actually being modified, but their references are instead, well…that’s totally right.

I really liked the way that Michael Morin explained this in his post on frozen objects:

When you freeze a hash, it just freezes the references to the objects it already has, it doesn’t actually freeze the objects themselves. The frozen hash won’t know the difference here. This is an important thing to understand about freezing objects, it’s not recursive. All freezing does is prevent the references the object holds from being modified.

While we can’t unfreeze “frozen” objects, this particular quality gives us a little bit leeway to get around the lack of a thaw method.

Chill Out, But Don’t Freeze

There are a few different ways to change an immutable frozen object (which, hello, aren’t actually frozen, just their references are). One thing you can do is create a duplicated object, which will not be frozen and therefore, completely mutable:

1
2
3
4
5
6
7
8
9
10
11
12
2.2.0 :009 > x.dup
 => {1=>"a", 2=>"b", 3=>"c"}
2.2.0 :010 > y = _
 => {1=>"a", 2=>"b", 3=>"c"}
2.2.0 :011 > y.frozen?
 => false
2.2.0 :012 > x.frozen?
 => true
2.2.0 :013 > y[1] = 'b'
 => "b"
2.2.0 :014 > y
 => {1=>"b", 2=>"b", 3=>"c"}

Keep in mind, however, that this only changes the data in the duplicated object, not the original one! So…what now? Well, since only the reference to an object is frozen, that means that you can modify the actual data inside of the frozen object:

1
2
3
4
5
6
2.2.0 :015 > x[1] << 'bc'
 => "ab"
2.2.0 :016 > x
 => {1=>"abc", 2=>"b", 3=>"c"}
2.2.0 :017 > x.frozen?
 => true

Weird, right!? Our x variable, which points to the Hash object, is still very much frozen. But, we can modify the data and values inside of the object itself. Since we don’t want to make an actual copy of the variable, we can instead modify the objects held within that variable (in our case, we’re directly modifying the String object, which is a the value of a key in this frozen Hash)

Freezing Out Those Error Messages

Cool, so this I guess this is a thing in Ruby. But wait – it’s also a pretty important thing in Rails! Objects in Rails are infinitely more complicated than in Ruby, mostly because you’re dealing with ActiveRecord and its methods. Whenever you’re trying to delete an ActiveRecord object, you depend on the destroy method.

For example, I use this method in my bookstore app, and I have a delete action in my BooksController that contains code that looks something like this:

1
2
@book = Book.find(1)
@book.destroy

Now if I wanted to modfiy this book’s type after I delete it, I’d get an error:

1
2
[1] pry(main)> @book.type = 'hardcover'
TypeError: can't modify frozen hash

Why is this? Well, even though I deleted the object, it still exists in the @book variable, which still retains all the attributes of the object. Once I destroy the record, the object became frozen. Since the object’s attributes hash is frozen, when I try to modify it, I get this error.

The Rails documentation explains how it implements the freeze method, stating that destroy method:

Deletes the record in the database and freezes this instance to reflect that no changes should be made (since they can’t be persisted).

To avoid creating a @book object that’s frozen and still hanging around for no apparent reason, I could instead implicitly delete the object in a single line. Either this:

1
Book.find(1)destroy

or

1
Book.destroy(1)

would work.

Alternatively, I could just use the delete method, which doesn’t instantiate an object, but isntead directly removes the row from the database.

Frozen objects are in front of us everyday. Ruby actually uses the freeze method when you copy certain objects. According to this post, when you use a string as a key of a Hash object, the string is copied, frozen, and then that duplicated copy is used as the key of the hash. This is pretty cool because this means that even if the original string is modified, the duplicated one isn’t affected because it has been frozen! The same goes for filenames: internal operations of an application rely on a frozen copy of a filename, rather than the original one.

Once you understand the functionality of freeze, it becomes a lot easier to understand those weird error messages. Soon, you’ll no longer fear the ice, but instead bask in the sun. Or…something more poetic.

Anyways, here’s a dancing snowman:

tl;dr?

  • You can use the freeze method to make mutable objects immutable. To check whether an object can be modified or not, use the frozen? method, which will return a boolean value.
  • Certain Ruby objects, such as integers, floats, and symbols, are always frozen and cannot be modified.
  • Someone made a melt gem – for realsies!
  • Some people are not big fans of the weird functionality of Ruby freeze. But it’s still worth it to learn how it works, especially since it can be super helpful for debugging Rails apps.