Ruby Freeze Method – Understanding Object Mutability

What does it mean for an object to be mutable?

Don’t let fancy words confuse you, “mutability” just means that an object’s internal state can be changed. This is the default of all objects, excluding those that have been frozen, or those that are part of a list of special objects.

In other words, not all objects in Ruby are mutable!

For example:

It doesn’t make any sense for numbers or symbols, or even true or false (which are also objects) to change.

The number 1 is always going to be 1.

But other objects, especially those that are meant to store data, like Array or Hash objects, should have the ability to change for performance reasons.

What’s the alternative?

You can make a new copy of an object with the changes & then return this new object, leaving the original object intact.

If arrays were immutable and you wanted to change just one element of an array, you would have to copy all the data, including the elements that didn’t change.

Imagine having to copy a one million (or more) element array every time you had to make any change, doesn’t matter how small! Not very efficient…

Anyway.

Let’s look a little deeper into how mutability works in Ruby.

Mutability & Variables as Pointers

There is a category of programming errors that are caused by the combination of two things:

  • Mutable objects.
  • The fact that variables don’t contain data directly, but a reference to where this data is stored.

One way these errors show themselves is when you try to ‘alias’ a variable.

Here is an example:

name       = "Peter"
other_name = name

puts other_name
# "Peter"

In this example, both name and other_name contain a reference to the same string object. You can use either to display or modify the contents of this string.

ruby freeze

The problem appears if we treat other_name like a copy of the string.

other_name[0] = 'T'

name       # "Teter"
other_name # "Teter"

Since both variables point to the same string, we just changed “Peter” to “Teter”. That’s a problem because we probably meant to keep “Peter” around.

Cloning Objects

One way to deal with this issue is to use the dup method.

This will tell Ruby to give you a copy of the object. There is also a clone method, which in addition to giving you a copy of the object, it copies the frozen status & any singleton methods defined on the object.

Let’s see an example:

numbers = [1, 2, 3]

more_numbers = numbers.dup
more_numbers << 4

numbers      # [1, 2, 3]
more_numbers # [1, 2, 3, 4]

In this example, you can see how the original numbers array remained unchanged. Try removing that dup call on the third line and see what happens 🙂

The Ruby Freeze Method

Another way to keep an object safe from unwanted changes is to ‘freeze’ it. Any Ruby object can be frozen by using the freeze method.

When an object is frozen, any attempt to change this object will result in a RuntimeError exception.

Note: You can use the frozen? method to check if an object is frozen or not.

Example:

animals = %w( cat dog tiger )
animals.freeze

animals << 'monkey'
# RuntimeError: can't modify frozen Array

One thing to keep in mind is that this will only freeze one object, in this example the array itself, which prevents us from adding or taking away items from it. But the strings inside the array are not frozen, so they can still be changed!

animals[1][0] = 't'
# => ["cat", "tog", "tiger"]

If you want to freeze the strings you need to call freeze on them. Like this: animals.each(&:freeze).

Frozen Strings

Mutable objects also have an impact on performance, especially strings. The reason is that there is a good chance that in a large program the same string is used multiple times.

Ruby will create a new object for every string, even if two strings look the same, or in other words, they have the same ‘content’. You can easily see this happen in irb if you use the object_id method.

Here is an example:

a = 'test'
b = 'test'

a.object_id # 76325640
b.object_id # 76317550

This is a problem because these objects are consuming extra memory and extra CPU cycles.

Starting with Ruby 2.1, when you use frozen strings, Ruby will use the same string object. This avoids having to create new copies of the same string. Which results in some memory savings and a small performance boost.

Rails makes extensive use of frozen strings for this reason. For example, take a look at this PR.

This prompted the Ruby development team to start considering moving strings into immutable objects by default. In fact, Ruby 2.3, which was just released a few days ago, includes two ways to enable this for your project.

One is to include # frozen_string_literal: true at the top of every file where you want strings to be immutable. And the other is to use a command-line argument --enable=frozen-string-literal.

Immutable strings by default will probably land in Ruby 3.0.

Now don’t go crazy and start freezing all your strings in your app. You only want to do this for strings that are used hundreds of times to see some sort of benefit. Having said that, here is a tool that you can use to find potential strings to freeze.

Know Your Methods

Not all the methods in a mutable object will actually change the object, for example, the gsub method will return a new string, leaving the original untouched.

Some of these methods have an alternative version which does change the original object in-place, which is often more efficient. These methods often end with an exclamation symbol ! to indicate their effect.

Two examples of these ‘bang’ methods are gsub! and map!.

Note this:

A method ending in ! doesn’t always mean that it’s a ‘method that changes an object’.

In more general terms, the ! symbol is used to denote ‘danger’. One example of this is the exit! method, which will exit the program immediately, ignoring any exit handlers.

There are also methods that change the object and don’t end with a ! symbol. For example: delete, clear, push, concat, and many more.

Wrapping Up

Mutability can be a tricky subject, but since you read this post you are now much better prepared to deal with it. Check the Ruby documentation if you aren’t sure what a method is doing, this will help you avoid issues.

I hope you found this article informative, please share it with your friends so they can enjoy it too. Also join my newsletter below so you don’t miss more content like this when it comes out!

4 thoughts on “Ruby Freeze Method – Understanding Object Mutability”

  1. One thing to be careful with when using #dup, it can be rather dup(ious). Correct me if I’m wrong but #dup will only copy the reference to the array, but not the elements inside the array. In your example its fine because ints are not mutable. However, if you have a nested array (like you mentioned arrays are mutable) and try to use #dup, things will get hairy.

    Heres what I mean: https://gist.github.com/gabrie30/55481c98287782c7702a

    Ruby by default only performs a shallow dup. So just remember you will need to roll your own deep dup for certain objects.

  2. I do not agree to this statement:

    “Imagine having to copy a one million (or more) element array every time you had to make any change, doesn’t matter how small! That wouldn’t be very efficient, would it?”

    There are two reasons:

    The wording gives the impression that you need to copy all 1,000,000 String instances held in the Array while in Ruby you actually only have to copy 1,000,000 references.

    This very concept is used in many functional programming languages and even non functional languages like Java to implement side effect free functions and get thread safe yet still efficient (i.e. lock free) data structures.

    With modern memory management like in the JVM this approach can actually be quite efficient for applications with a lot concurrency. Of course there is a price to pay but the world is not black and white.

Comments are closed.