SafeDup

This tiny gem implements a version of dup called safe_dup. In Ruby, if an attempt is made to dup 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 dup operation must be applied with great care.

Unlike the standard dup method, the safe_dup method does not throw an exception when sent to un-clonable value objects like 42 or true. These values simply return themselves. This is deemed correct because those types of objects are immutable and do not need to be duped. Instead of raising an exception, the code returns the immutable object instead.

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

Finally, this gem does not monkey patch the behavior of the dup method. Modifying such a crucial method was considered too risky. Instead, the safe_dup 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_dup'

And then execute:

$ bundle

Or install it yourself as:

$ gem install safe_dup

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_dup'

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

foo = my_object.safe_dup

instead of

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

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

Demo

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

$ rake console

Performance

A reasonable question to raise is "How does safe_dup 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 duplicated. The benchmarking code follows:

require "benchmark/ips"
require 'safe_dup'

class Array
  def use_dup
    self.map do |element|
      begin
        element.dup
      rescue TypeError
        element
      end
    end
  end

  def use_safe_dup
    self.map {|element| element.safe_dup }
  end
end

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

Benchmark.ips do |x|
  x.report("Dup with standard dup method") { X.use_dup }
  x.report("Dup with the safe dup method") { X.use_safe_dup }
  x.compare!
end

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

C:\Sites\safe_dup>ruby bench\bench.rb
Warming up --------------------------------------
Dup with standard dup method
                         1.245k i/100ms
Dup with the safe dup method
                        34.118k i/100ms
Calculating -------------------------------------
Dup with standard dup method
                         12.862k (± 6.2%) i/s -     64.740k
Dup with the safe dup method
                        533.927k (± 6.5%) i/s -      2.661M

Comparison:
Dup with the safe dup method:   533926.5 i/s
Dup with standard dup method:    12861.7 i/s - 41.51x slower

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

C:\Sites\safe_dup>ruby bench\bench.rb
Warming up --------------------------------------
Dup with standard dup method
                         4.969k i/100ms
Dup with the safe dup method
                        38.025k i/100ms
Calculating -------------------------------------
Dup with standard dup method
                         54.786k (± 7.1%) i/s -    273.295k
Dup with the safe dup method
                        567.814k (±10.1%) i/s -      2.814M

Comparison:
Dup with the safe dup method:   567814.3 i/s
Dup with standard dup method:    54785.8 i/s - 10.36x slower

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

$ ruby bench/bench.rb
Warming up --------------------------------------
Dup with standard dup method
                         3.662k i/100ms
Dup with the safe dup method
                        28.310k i/100ms
Calculating -------------------------------------
Dup with standard dup method
                         39.437k (± 6.1%) i/s -    197.748k
Dup with the safe dup method
                        450.558k (± 3.9%) i/s -      2.265M

Comparison:
Dup with the safe dup method:   450557.6 i/s
Dup with standard dup method:    39436.6 i/s - 11.42x slower

Results: ruby 2.3.3p222 (2016-11-21 revision 56859) [i386-mingw32]

Warming up --------------------------------------
Dup with standard clone method
                         5.866k i/100ms
Dup with the safe clone method
                        23.721k i/100ms
Calculating -------------------------------------
Dup with standard clone method
                         70.492k (± 0.4%) i/s -    357.826k in   5.076221s
Dup with the safe clone method
                        523.478k (± 1.1%) i/s -      2.633M in   5.030544s

Comparison:
Dup with the safe clone method:   523477.8 i/s
Dup with standard clone method:    70491.9 i/s - 7.43x  slower

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

Contributing

Plan A

  1. Fork it ( https://github.com/PeterCamilleri/safe_dup/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.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the fully_freeze project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.