I'm going to start from a few basic principles, because Ruby works
differently than you think it does, and that's kinda obvious from how
you're not getting this.
Before I get started, I think that the article you read has done you a
great disservice because the authors raised a point that isn't actually
a problem in practice in Ruby. (And even in C++, it is pretty easy to
subvert the const-ness of a variable. It's a little harder to subvert
the const-ness of a reference, but not impossible.)
Most of what I'm going to say here is similar to what Jim Weirich said
in his "Shoeboxes and Bindings"[1] essay. You probably came from the
class of languages descending from C, where each variable is a place in
memory. For non-pointer operations, simple assignment is duplication:
std::string a, b;
a = b = "Hello";
In this C++ example, you have two shoeboxes that each contain a copy of
a string, "Hello":
a [ "Hello" ]
b [ "Hello" ]
In C and C++ you can use pointers and references to alias these
shoeboxes.
std::string& c;
std::string* d;
c = a;
d = &b;
c += ", world!";
(*d) += ", program!";
In this C++ example, you have two more shoeboxes. These are special
shoeboxes in that they don't contain strings; they contain the addresses
of strings.
c [ address-of a ] -> a [ "Hello, world!" ]
d [ address-of b ] -> b [ "Hello, program!" ]
In C++, the variable d has its own storage. Because it's a pointer to
std::string, it's going to be sizeof(void*). It's even possible to have
a pointer to d:
std::string** e = &d;
Which is:
e [ address-of d ] -> d [ address-of b ] -> b [ "Hello, program!" ]
It's turtles all the way down. The only time that it gets a little dicey
is when you allocate memory on the heap:
std::string* f = new std::string("Greetings, program!");
std::string** g = &f;
But even that is pretty easy to deal with, conceptually. In this case,
we're given a handle to the memory -- the address. It might look like:
f [ address-of g ] -> g [ address ] -> [ "Greetings, program!" ]
Like I said, pretty basic stuff.
...
And completely *not* the way that Ruby works.
Ruby variables are closer to the behaviour of C/C++ pointers and
references, except that they are not shoeboxes themselves. This is an
important thing to remember. Jim calls them "bindings." I call them
"labels." Either way, they're essentially local names for values.
The fundamental in Ruby is not a variable, but an object:
"Hello"
This is an object. It can have things done to it and it can be passed
around. However, this object is anonymous. We don't know its name, or it
doesn't have one. Because it is anonymous and isn't bound anywhere, it
may end up going out of scope quickly and be eligible for garbage
collection. So, we give an object a name:
x = "Hello"
Now, we can manipulate the object without fear that it's going to go out
of scope and be garbage collected when we need to use it. The
representation is something like:
x -> [ "Hello" ]
Note the difference in the way that I showed this from the way that I
showed the C++ example:
x -> [ "Hello" ]
c [ address-of a ] -> a [ "Hello, world!" ]
g -> [address of f ] -> f [ address ] -> [ "Greetings, program!" ]
The form with 'f' is a little closer to the Ruby view of variables, but
it's still a piece of storage that can have its address taken as well,
as the 'g' shows. Well, that won't work with Ruby.
x = y = "Hello, muddah."
That would look something like:
x ------v
[ "Hello, muddah." ]
y ------^
We now have the object "Hello" referred to by two different names. But
if I assign to y again:
y = "Hello, faddah."
I the two separate names pointing to two objects.
x -> [ "Hello, muddah." ]
y -> [ "Hello, faddah." ]
Pretty simple so far, and clear. Ruby throws one loop at us, but it's
not nearly as bad as the object/primitive distinction that Java gives.
Some Ruby objects exist exactly once and are never garbage collected.
This is partially for performance reasons, but it also makes sense. The
values that do this are Fixnums (31-bit signed integer values), Symbols,
and true, false, and nil. These values are called "immediate" values.
These objects are immutable. It is possible for a class to simulate
immediacy without the object itself being a full-on immediate value. I
believe that Bignums are implemented this way.
99% of the time, the distinction between immediate and reference values
with the binding approach that Ruby takes to variables. You simply bind
a variable to an object; you don't make a storage space that you then
put an object or a reference to that object to. There are no shoeboxes.
Ruby only has to worry about memory management of objects; you don't
have to worry about any of it.
Now, how does this apply to constants? Well, in Ruby, constants are just
special variables. The bindings *can* be changed, but Ruby will complain
on binding them to different objects. Unless a mutable object is frozen,
though, the contents of the object referred to by object can be
modified.
Note the important words on that last sentence: mutable object.
Immediate values -- Fixnums -- are immutable. That's why 10.freeze
(which is what x = 10; x.freeze does) is a no-op.
Hopefully, that helps.
-austin
[1] http://onestepback.org/index.cgi/2003/12/15
···
--
Austin Ziegler * halostatue@gmail.com
* Alternate: austin@halostatue.ca