In Progress
Unit 1, Lesson 21
In Progress

Dup and Clone

Why does Ruby have two different methods for copying objects? In this episode, we’ll delve into the differences between dup and clone, and you’ll learn how to choose which one to use.

Video transcript & code

Say we have a Ruby class representing a shopping list.

While individual shopping trips may vary, we find that our lists usually contain certain common staples. So we construct a "master list" to use as a template for our shopping lists. It comes pre-filled with some items we always need.

ShoppingList = Struct.new(:name, :items)
MASTER_LIST = ShoppingList.new("Master List", [
                                 "Bread",
                                 "Milk",
                                 "Beer"])

On a given day, we copy the template to a list for that day. We give the copy its own name. And we add any extra needed items.

There's a bug here, and you've probably already spotted it. Instead of copying the master list, we just assigned another name to it. So all our changes to today's list are also made to the master list.

require "./shopping_list"

today_list = MASTER_LIST

today_list.name = "Wednesday Shopping"
today_list.items << "Eggs"

MASTER_LIST.name
# => "Wednesday Shopping"
MASTER_LIST.items
# => ["Bread", "Milk", "Beer", "Eggs"]

Before we fix this, we decide to fix the master list so that it will prevent us from making this kind of mistake again.

So we freeze it.

Now our incorrect code immediately fails the first time it tries to modify the master list.

require "./shopping_list"

MASTER_LIST.freeze
today_list = MASTER_LIST

today_list.name = "Wednesday Shopping" # ~> RuntimeError: can't modify frozen ShoppingList

# ~> RuntimeError
# ~> can't modify frozen ShoppingList

Now that we have an early warning system in place, let's fix the bug. Instead of just assigning the master list object to a new variable, we want to copy the master list to a new object. To do this, let's send the clone message.

We run the code and… huh. We get the same error!

require "./shopping_list"

MASTER_LIST.freeze
today_list = MASTER_LIST.clone

today_list.name = "Wednesday Shopping" # ~> RuntimeError: can't modify frozen ShoppingList

# ~> RuntimeError
# ~> can't modify frozen ShoppingList

What happened? Well, as it turns out, clone is how we tell Ruby to make as identical and accurate a copy of an object as it possibly can. Ruby tries it's best, and copies everything about the object… including its frozen state!

require "./shopping_list"

MASTER_LIST.freeze
today_list = MASTER_LIST.clone

MASTER_LIST.frozen?             # => true
today_list.frozen?              # => true

Maybe we don't want quite so faithful a copy for this purpose. It feels like what we want is a copy of the original object's value, but not of object metadata such as its frozen state.

Ruby has another method for this kind of copy, and it's called dup.

Running this code with dup instead of clone, we can see that the duplicate object is not frozen.

require "./shopping_list"

MASTER_LIST.freeze
today_list = MASTER_LIST.dup

MASTER_LIST.frozen?             # => true
today_list.frozen?              # => false

This means that we can now modify the attributes of today's list, while the original remains static.

require "./shopping_list"

MASTER_LIST.freeze
today_list = MASTER_LIST.dup

today_list.name = "Wednesday Shopping"
today_list.name
# => "Wednesday Shopping"
MASTER_LIST.name
# => "Master List"

So, is copying the frozen status the only difference between dup and clone?

Not quite. clone doesn't just copy an object's frozen state. It also copies any singleton-class modifications for that object.

This is a distinction that, frankly, isn't going to matter to you in ordinary application programming contexts. It's something that matters more for things like building new programming tools.

For example, let's say we want to create a tool that lets us play around with Ruby objects and then later revert our changes to an earlier point.

Here's some code which does just that. It's a monkey-patch to the Binding class, which is the class Ruby offers for introspecting and modifying local variables.

I'm not going to go into this code in detail. The important thing right now is that it enables us to "checkpoint" the current state of local variables. And the checkpoint method uses clone to save exact copies of the variable values—including their frozen state and singleton classes.

class Binding
  def checkpoints
    Thread.current[:binding_checkpoint] ||= []
  end

  def checkpoint
    cp = {}
    local_variables.each do |varname|
      cp[varname] = local_variable_get(varname).clone
    end
    checkpoints.push(cp)
  end

  def undo
    cp = checkpoints.pop
    cp.each do |varname, value|
      local_variable_set(varname, value)
    end
  end
end

Here's how we might use it. We create a new object, and call it a duck.

Then we make it talk like a duck. Let's try out this new method.

Before we go on, we save a checkpoint of the current local variable state.

Next we make the object move like a duck. Again, let's make sure this works.

Now we invoke the undo method from our special Binding modification. This reinstates the cloned copies from our earlier checkpoint.

At this point, our duck can quack…

…but it can no longer waddle.

require "./checkpoints"

duck = Object.new
def duck.speak
  "Quack!"
end

duck.speak
# => "Quack!"

binding.checkpoint

def duck.move
  "Waddle waddle"
end

duck.move
# => "Waddle waddle"

binding.undo

duck.speak
# => "Quack!"

duck.move # ~> NoMethodError: undefined method `move' for #<Object:0x0000000280e2e0>
# =>

# ~> NoMethodError
# ~> undefined method `move' for #<Object:0x0000000280e2e0>
# ~>
# ~> xmptmp-in31140F1f.rb:25:in `<main>'

If we change clone to dup, the outcome changes. We can see that now the saved duck can't even quack. That's because dup doesn't preserve singleton class state.

binding.undo

duck.speak # ~> NoMethodError: undefined method `speak' for #<Object:0x0000000286e4d8>

What this illustrates is that with dup, we get a new object with the same type and instance variable state. But if we want to also capture object metadata like frozen state and singleton class modifications, we must use clone.

Practically speaking, where does this leave us? Well, the guideline for using these methods is actually pretty straightforward: when in doubt, use dup. In most cases, it will be what you're looking for. Only use clone if dup doesn't give you the copying semantics you need.

Happy hacking!

Responses