A safe, fast, exception free clone method that demonstrates the use of inheritance and safe monkey patching.
Ruby
Switch branches/tags
Clone or download

README.md

SafeClone

This tiny gem implements a version of clone called safe_clone. In Ruby, if an attempt is made to clone an immutable data item like a number, an error occurs. The justification for this uncharacteristic strictness is not at all clear, but it does mean that the clone operation must be applied with great care.

Unlike the standard clone method, the safe_clone method does not throw an exception when sent to un-clonable value objects like 42 or true. These values simply return themselves. This is correct because those types of objects do not need to be cloned. Instead of having a fit, the code just works!

On a note about performance, this gem does not just rescue the exceptions normally generated by clone, it prevents them from occurring and wasting time in the first place.

Finally, this gem does not monkey patch the behavior of the clone method. Modifying such a crucial method was considered too risky. Instead, the safe_clone method is introduced. This is done to reduce the possibility of breaking existing code that often occurs when monkey patching goes too far.

Family Overview

This gem is a member of a family of four gems that all provide data copying services in a safe, easy to use format. The following outlines the available gems and how to chose from among them.

Depth / Action Need to copy all. Need to copy data only.
Need a shallow copy require 'safe_clone' require 'safe_dup'
Need a full copy require 'full_clone' require 'full_dup'


Notes

  • Since none of these gems override the default clone and dup methods, the default behaviors remain available. Further, if multiple, differing requirements exists, more than one family member gem may be employed in the same project without fear of conflict.
  • If multiple family gems are employed, they will each need to be installed and required into the application. See below for details.
  • Meta-data attributes include the frozen status and singleton methods. However the tainted status is always copied.

Installation

Add this line to your application's Gemfile:

gem 'safe_clone'

And then execute:

$ bundle

Or install it yourself as:

$ gem install safe_clone

The safe_dup gem is at: ( https://rubygems.org/gems/safe_dup )
The safe_clone gem is at: ( https://rubygems.org/gems/safe_clone )
The full_dup gem is at: ( https://rubygems.org/gems/full_dup )
The full_clone gem is at: ( https://rubygems.org/gems/full_clone )

Usage

require 'safe_clone'

then, in those places where regular clone was problematic, use:

foo = my_object.safe_clone

instead of

begin
  foo = my_object.clone
rescue TypeError
  foo = my_object
end

It is actually pretty easy to determine where safe_clone needs to be used. It's those places where the clone method is generating unwanted exceptions.

Demo

A test bed for experimenting with the safe_clone gem is available as a rake task:

$ rake console

Performance

A reasonable question to raise is "How does safe clone compare with just catching the exception and handling it?" The benchmark sets a a realistic scenario where an array (whose contents may be varied) is having its contents cloned. The benchmarking code follows:

require "benchmark/ips"
require 'safe_clone'

class Array
  def use_clone
    self.map do |element|
      begin
        element.clone
      rescue TypeError
        element
      end
    end
  end

  def use_safe_clone
    self.map {|element| element.safe_clone }
  end
end

X = ["Test", :test, 43, true, nil, false]

Benchmark.ips do |x|
  x.report("Clone with standard clone method") { X.use_clone }
  x.report("Clone with the safe clone method") { X.use_safe_clone }
  x.compare!
end

Results: ruby 1.9.3p484 (2013-11-22) [i386-mingw32]

C:\Sites\safe_clone>ruby bench\bench.rb
Warming up --------------------------------------
Clone with standard clone method
                         1.247k i/100ms
Clone with the safe clone method
                        35.027k i/100ms
Calculating -------------------------------------
Clone with standard clone method
                         12.957k (± 5.8%) i/s -     64.844k
Clone with the safe clone method
                        534.740k (± 8.9%) i/s -      2.662M

Comparison:
Clone with the safe clone method:   534740.1 i/s
Clone with standard clone method:    12956.6 i/s - 41.27x slower

Results: ruby 2.1.6p336 (2015-04-13 revision 50298) [i386-mingw32]

C:\Sites\safe_clone>ruby bench\bench.rb
Warming up --------------------------------------
Clone with standard clone method
                         4.945k i/100ms
Clone with the safe clone method
                        38.109k i/100ms
Calculating -------------------------------------
Clone with standard clone method
                         54.491k (± 7.3%) i/s -    271.975k
Clone with the safe clone method
                        569.236k (±10.2%) i/s -      2.820M

Comparison:
Clone with the safe clone method:   569236.4 i/s
Clone with standard clone method:    54491.3 i/s - 10.45x slower

Results: ruby 2.2.3p173 (2015-08-18 revision 51636) [i386-cygwin]

Peter Camilleri@NCC1701G /cygdrive/c/sites/safe_clone
$ ruby bench/bench.rb
Warming up --------------------------------------
Clone with standard clone method
                         3.698k i/100ms
Clone with the safe clone method
                        28.999k i/100ms
Calculating -------------------------------------
Clone with standard clone method
                         40.076k (± 5.1%) i/s -    203.390k
Clone with the safe clone method
                        481.524k (±10.0%) i/s -      2.407M

Comparison:
Clone with the safe clone method:   481524.1 i/s
Clone with standard clone method:    40075.6 i/s - 12.02x slower

Overall: Shorter code and faster. Winner, winner, chicken dinner!

Contributing

Plan A

  1. Fork it ( https://github.com/PeterCamilleri/safe_clone/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Plan B

Go to the GitHub repository and raise an issue calling attention to some aspect that could use some TLC or a suggestion or an idea.