Ruby I don't like #3 - Object#freeze
Published over 4 years ago

Object#freeze annoys me. Not a lot, but enough to bitch blog about it. So, freeze lets you make sure no one else modifies your precious little object :

>> a = "hello"
>> a.freeze
>> a << "wtf"
TypeError: can't modify frozen string
        from (irb):23:in `<<'
        from (irb):23

However, freeze does not protect the variable. It only protects the value.

>> a = "hello"
>> a.freeze
>> a += "wtf"
=> "hellowtf"

The weird behaviour is even more visible when you’re dealing with arrays :

>> x = ["hello", "freedom"]
>> x.freeze
>> x << "world"
TypeError: can't modify frozen array
        from (irb):6:in `<<'
        from (irb):6
>> x[0] << "wtf"
>> x
=> ["hellowtf", "freedom"]

It’s even weird with hashes :

>> a = {:x => 1}
>> a.freeze
>> a[:x] = 2
TypeError: can't modify frozen hash
        from (irb):11:in `[]='
        from (irb):11

However, above are not really the primary reasons I don’t like freeze. It’s the fact that you cannot unfreeze an object without using something like evil.rb. And this goes against a lot of things Ruby stands for in my book. Ruby is never about defensive programming. Even where it tries to save you from yourself, there are always proper ways you can overcome the restriction. For example, private methods and send. If you want to restrict programmers, Ruby is not for you. Use Java/Python/whatever. Not Ruby. Ruby is not meant for preventing idiots from shooting their leg.

Taking a real example :

module ActiveSupport
  module Testing
    module Performance
      DEFAULTS =
        if benchmark = ARGV.include?('--benchmark')  # HAX for rake test
          { :benchmark => true,
            :runs => 4,
            :metrics => [:process_time, :memory, :objects, :gc_runs, :gc_time],
            :output => 'tmp/performance' }
        else
          { :benchmark => false,
            :runs => 1,
            :min_percent => 0.01,
            :metrics => [:process_time, :memory, :objects],
            :formats => [:flat, :graph_html, :call_tree],
            :output => 'tmp/performance' }
        end.freeze

Here’s a code from Rails performance tests. As you can see, it defines a hash with config variables based on benchmark or profile mode. And then freezes the hash. Assuming you’re benchmarking, DEFAULTS[:runs] determines how many times Rails should run the test in a loop :

DEFAULTS[:runs].times { run_test_with_benchmarking "whatever test" }

Now many times when I’m benchmarking and want to increase the number of times a test is ran, I just want to do something like :

class SpeedTest < ActionController::PerformanceTest
  DEFAULTS[:runs] = 1000

  def test_some_method
    Model.some_method
  end
end

However, that’s not possible thanks to the freeze. I do know that changing DEFAULTS[:runs] is not a public API yada yada yada. But it’s Ruby and I’ll change whatever the fuck I want to. I can understand certain cases where people use freeze to prevent silly errors. That’s probably OK to a certain extent. But remember, if you design your software for idiots, only idiots will use it.