RubyGuides
Share this post!

Understanding Mutable Objects & Frozen Strings

Let’s talk about mutability.

What does it mean for an object to be mutable? Don’t let the fancy word deceive you, it just means that the object state can be changed.

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.

But other objects, especially those that are meant to store data, like Array or Hash objects, make a lot more sense because it would not be very efficient otherwise.

The alternative to mutating an object, but still being able to get an updated version of the object, is to make a new copy of the object with the changes and 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! That wouldn’t be very efficient, would it?

Mutability & Variables as Pointers

There is a category of programming errors which 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:

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.

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.

5 Tips to Become a Better Ruby Developer (must read!)

Cloning Objects

One way to deal with this issue is to use the dup method, that will tell Ruby to give us 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:

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 it 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:

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!

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:

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!.

Also, note that 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 is 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. Don’t hesitate to consult 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 comments
Abhimanyu Aryan (@aryan_chat) says last year

amazing guide

Jay Gabez says last year

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.

    Jesus Castello says last year

    Yeah you make a good point. Thanks for commenting.

rklemme says last year

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