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 :
1 2 3 4 5 6 |
>> 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.
1 2 3 4 |
>> a = "hello" >> a.freeze >> a += "wtf" => "hellowtf" |
The weird behaviour is even more visible when you’re dealing with arrays :
1 2 3 4 5 6 7 8 9 |
>> 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 :
1 2 3 4 5 6 |
>> 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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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 :
1 2 3 4 5 6 7 |
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.






I completely agree, as you know. The fact is, there’s no valid reason to stop people from doing it. A program is incorrect if it does not do what is intended. This has absolutely nothing to do with levels of protection; it is related to the code that has been written by a programmer. Hiding dangerous possibilities from programmers slows their learning, which is plain bad business. Furthermore, as you describe here, it also slows down experts, who may have a valid reason to alter something. In reality, all that freeze does is make use jump through really ridiculous hoops, like so:
Of course, this will only work if you’re using relative constant lookups, and so is prone to other breakage. Furthermore dup is only shallow, so modifying internal contents of this hash is also potentially dangerous, as they are the same objects as found in PerformanceTest::DEFAULTS.
This means in order to merely experiment, I must analyse not just the contents and predicted meaning of the values in the constant, but I must also decide where errors could be introduced in code I did not write, because that code may make hard assumptions about the contents of its frozen constants.
The most common (almost sane) reasoning I hear for wanting to freeze, is to fail fast if someone does try to modify these objects. Ruby already provides warnings for constant redefinitions. If your focus is to identify problems early, and you’re not running with warnings on, then your reasoning is inherently limited to an emotional view, not one which has taken a full view of the stack you use. Of course these warnings do not cover the final example above, however, there are other impacts seen in the above code, caused by the use of freeze.
Lets look at how DRY it is, and why it is not. It is repeating itself because it’s frozen. I would prefer to write the defaults setup like so:
This has two prominent affects to the code:
1. The merge can happen in runtime code, rather than being hacked into the constant definition. This has a side effect of removing embarrassing and unprofessional comments from the source.
2. The clarity of both the differences is profoundly enhanced, as well as the conceptual separation between the two different states.
Do you think ActiveSupport:Memoizable.memoize shouldn’t freeze the return value?
Yeah. I don’t think #freeze should be used anywhere.
I always thought #freeze was invented so that the interpreter could make certain optimizations. For instance, a method which relies solely on frozen objects could be automatically memoized by the interpreter without changing external behaviour. I imagine the garbage collector could implement similar shortcuts. In an optimizing context, judicious use of “#freeze” makes sense: sure, it restricts what you can do, but for a good reason. (Full disclaimer: I’m one of those guys who writes “const” all over his C++.)
But I’m perplexed: Ruby’s canonical implementations have got to be the least-optimized interpreters out there; why was #freeze invented when the optimizations weren’t?
It’s slow enough (IIRC) that it’s not really worth doing, but I think youse guys are forgetting something: Freeze isn’t just about “protection”. It can also be used to simulate immutability. Especially in Strings.
Which is a big deal. You will never see a “PORO” O/RM for Ruby without some big caveats without it. Threading will be harder (and slower) without it (since you have to mutex even simple “primitives”).
Immutability isn’t so much a safety measure as it is a design decision enabling higher performance, higher level abstractions.
You might argue there’s nothing preventing using mutable primitives in the same manner, but that falls down pretty quickly when you consider there’s just no practical way you’re going to train a whole community not to use
String#<<on values that get passed to your libraries.Ruby isn’t even consistent here. Fixnum is immutable, Symbol is immutable, String isn’t, Date isn’t. It’s just poor design.
To back-pedal a bit though, I wouldn’t have an issue getting rid of String#freeze though in favor of an alternative FixedString type and a String#to_fixed method.
It’s not that I want to protect anyone but myself. If you’re allowed to call mutators on objects I’m tracking, then I either have to accept that you’ll be able to create conditions I can’t handle, or go with a slower, more resource-intensive strategy like
#dup’ing values.BTW, last I looked AR just ignores the mutability problem in it’s change-tracking.
@Sam If you don’t want users to modify a string, why not just force symbols ?
Also, if I understand you correctly, when you’re tracking a string, you freeze it to force users to not use <<. That’s just sort of changing the place where the garbage is generated – from your library to user’s program. It’s true that less garbage will be generated, but it seems like you’re abusing #freeze because Ruby doesn’t provide any tracking mechanism.
Indeed, the
freezeabove is defensive. This constant is the default value for asuperclass_delegating_accessor :profile_options. If a subclass mutatesprofile_options, it’s actually silently changing the superclass’ options too.So freezing here is really a deeper sign of a bad API.
would be better declared as
where
profilesafely wraps up the options change.@Pratik: " If you don’t want users to modify a string, why not just force symbols ?"
IIRC, symbols are never garbage-collected.
Immutability is a good policy for many data structures. In fact, Ruby’s hash uses freeze internally. In order to keep you from mutating hash keys, hash freezes all string keys (defensive programming at work). Otherwise, mutating the string would cause the original hash code and actual value to fall out of sync.
{"foo" => "bar"}.each do |key, value| key << “!” # => TypeError: can’t modify frozen string endUsing strings as keys is common enough to enforce this policy.
There a few optimizations that I’ve seen in Ruby stdlib. In most cases they avoid cloning or duping an object if its already frozen and just return the original.
rb_str_new4(orig) VALUE orig; { VALUE klass, str; if (OBJ_FROZEN(orig)) return orig; …. }You can see the Hash uses this string constructor when it inserts keys.
rb_hash_aset(hash, key, val) VALUE hash, key, val; { rb_hash_modify(hash); if (TYPE != T_STRING || st_lookup(RHASH→tbl, key, 0)) { st_insert(RHASH→tbl, key, val); } else { st_add_direct(RHASH→tbl, rb_str_new4(key), val); } return val; }However this policy does not apply for anything besides strings. Probably because you can’t trust a complex object graph (even if frozen) to be immutable anyway. In that case you’d need to be sure to rehash after you mutated a key.
I mainly agree that I usually look to freeze when something else is wrong. Is more likely symbols should be used as keys instead of immutable strings or using immutable constants to hack around Ruby’s garbage collection for performance.
Knowing something is immutable and can’t be “thawed” is a powerful information for optimization. I definitely hope more optimizations for frozen objects go into the stdlib and into Ruby VMs.
BTW, if we had a thaw to complement freeze, would that make freeze more acceptable to you? Ruby does take alot of “defensive programming” measures like you mentioned. By default you can’t call private methods, but there is always a way around. It would be nice if you couldn’t just mutate frozen objects by default but we had a easy “I KNOW WHAT THE FUCK I’M DOING” override switch.
@Josh Yup that’s my primary complaint. freeze would be perfectly fine if thaw is in the standard Ruby.
As far as the example of the Arrays go. The array itself is a collection of objects. So each of these objects must be frozen individually. When you freeze the array all you are doing is freezing any changes being made to the total collection. I think it is correct to assume that this is not inherited by each item in the collection.
a0.freeze
a0 << “1234”
TypeError: can’t modify frozen string
Great article Pratik, as all of your “ruby I don’t like #” series.
Your phrase “Ruby is not meant for preventing idiots from shooting their leg” was specially hilarious. I instantly thought “so, Plaxico Burress (NY Giants – WR) was trying to use ruby, not dancing in a nightclub, when he shot accidentally in his leg, hahahaha”.
http://sportsillustrated.cnn.com/2008/football/nfl/11/29/burress.giants.ap/index.html Reference to the whole (and presumably real) story if anyone want to know about it
I’m not persuaded that #freeze is without it merits. I didn’t see anyone site this use case (which is a one I commonly use):
class Sweater
def initialize(colors = DEFAULT_COLORS) @colors = colors endDEFAULT_COLORS = [‘red’, ‘blue’, ‘green’]
attr_accessor :colors
end
sweater = Sweater.new
sweater.colors # => [‘red’, ‘green’, ‘blue’]
sweater.colors << ‘turqoise’
Sweater.new.colors # => [‘red’, ‘green’, ‘blue’, ‘turquoise’]
In this context, freezing the defaults constant is very useful. From this perspective, I also understand why you would want to freeze the result of memoize.
It seems that #freeze isn’t just to protect you from “idiots”. Projects do grow over time to magnificent sizes? And then a bug will inevitably be introduced somewhere because someone modified a value not realizing that it originated from a constant somewhere, and causing a strange, difficult to reproduce bug somewhere else.
Also, in defense of ruby for choosing to make fixnums, and floats immutable – I think it was more a matter compromise than poor design.
@Pratik regarding Symbols: For the same reason HashWithIndifferentAccess doesn’t. Because they aren’t GCed, so you’d eventually end up with a Symbol for every varchar in your database. Which means you might end up with a 20GB Ruby process. If it didn’t just crash first. ;-)
As far as abusing freeze, I’d disagree. I’m using freeze for exactly what it was intended for: To make an object immutable.
Though honestly were I to do it again today I’d just use JRuby or IronRuby and first try/hope that they provided fast access to native, immutable strings rather than use a Ruby String and freezing it.