Ruby I don't like #3 - Object#freeze
Published almost 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):11However, 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.