I’m deeply convinced that I’m a lion..

The Complete Guide to Create a Copy of an Object in Ruby: Part II

In this article, we’re going to explore the following topics:

  • the #initialize_copy method
  • the Marshal class
  • the #deep_dup method in Rails
Have a quick look to the Part I (3 minutes read)

Introduction

Ruby doesn’t handle deep copy by default.

Indeed, it only handles shallow copy through the Object#clone and Object#dup methods.

But what if we still need to make a deep copy of an object?

In this article, we’ll detail 3 ways to make a deep copy of an object in Ruby.

The #initialize_copy method

The #initialize_copy hook can be used to interact with the freshly created copy of a given object.

We can tweak this hook to achieve a deep copy.

First, let’s recap how to achieve a shallow copy in Ruby

Here the problem is that khabib and tony_fergusson share the same belt — UFC Lightweight Champion.

This means that if khabib changes the title.name attribute then the change will be propagated to the tony_fergusson.title.name one — as they share the same Title instance.

So, let’s implement the Fighter#initialize_copy to make a deep copy of khabib

In the above example, we define a Fighter#initialize_copy method that assign a shallow copy of the original_fighter.title (khabib) to self.title (tony_fergusson).

So, if we compare the 2 titles object_id we can see that it’s now 2 different instances of Title.

It works! That’s cool !

But, the problem is that we “tweak” a method that is called in the context of a shallow copy (via #dup and #clone) in order to process a deep copy.

Of course, it’s not a “serious” issue !

But let’s see what are the other alternatives.

The Marshal class

Object Marshalling is the concept of formatting the memory representation of an object to make it suitable to storage & deserialisation.

Basically, for a given object:

  • we serialise all its attribute values
  • we store the serialisation (for example, in a file or a variable)
  • we deserialise it at any time and get back a new instance of the serialised object with the same values.

In Ruby, the Object Marshalling logic is mainly implemented in the Marshal class.

Let’s have a look to the following example to detail how this class works

In the above example, we can see that the jd variable contains an instance of User.

Then we serialise this instance by using the Marshal.dump method.

This method returns a string that is the serialisation of the memory representation of the jd variable.

Finally, we deserialise this string by using the Marshal.load method.

This method returns a new instance of User that contains the exact same values as jd.

As we can see, jd and other_jd are two distinct instances of User.

Now, let’s see how to process a deep copy using Object Marshalling.

To do so, let’s refactor the Fighter and Title classes example

Here, we serialise the khabib instance of Fighter by using the Marshal.dump method.

Then we deserialise it by using the Marshal.load method and we store the result in the tony_ferguson variable.

As we can see, we achieve the same result as with the #initialize_copy method.

In effect, the title attribute of khabib and tony_fergusson are two distinct instance of Title.

There can be 2 main concerns to process a deep copy by using object marshalling:

  • it can become a very slow operation at scale
  • it’s not fully working with complex objects.

Otherwise, it’s a quite efficient way to achieve deep copy in Ruby.

Now let’s have a look to what Ruby on Rails proposes to handle deep copy.

Note that I will dig into Object Marshalling in Ruby in another article.

The #deep_dup method in Rails

The Ruby on Rails framework provides the Object#deep_dup method that allows you to create a deep copy of a given object.

This solution is implemented in ~30 LOC

1- The Object#duplicable? method returns true.

This means that an instance that contains the Object class in its ancestor chain is eligible to deep copy.

There is only few classes that are not “duplicable”:

NilClass FalseClass TrueClass Symbol Numeric BigDecimal Method Complex Rational

2- The Object#deep_dup method returns the return value of the dup method call or self if the object is not eligible to deep copy.

3- The Array#deep_dup method map through the calling array and calls #deep_dup on each element of the array.

4- The Hash#deep_dup method dup the calling hash and iterates through the calling hash.

Then, for each pair, it checks if the key is a frozen object.

If so, then only the value is #deep_dup.

Otherwise, the key and the value are #deep_dup.

There is also a set of #initialize_copy methods that are defined to handle complex objects (in ActiveRecord::Relation, etc..).

Voilà!

ONE MORE THING ⬇

Feel free to subscribe here: www.rubycademy.com


Thank you for taking the time to read this post :-)

Feel free to 👏 and share this article if it has been useful for you. 🚀

Here is a link to my last article:

The Complete Guide to Create a Copy of an Object in Ruby: Part I