Ruby is a dynamic language. There is no static compilation and it's very hard for the interpreter to know in advance who will eventually access and modify a variable.
Let's look at the following example. You have a string
str = "foo"
and then later in your code you have
str.upcase!
# => "FOO"
This example is pretty trivial even for a simple parser to understand that the string is mutated. But let's add some more complexity
str = "foo"
method = ["up", "case"]
str.send((method << "!").join)
# => "FOO"
This is producing exactly the same result of before, but the method is not statically coded in the script. Instead, it's the result of a computation then dynamically performed on the string at runtime.
But wait, let's make it even more complicated.
str = "foo"
method = [ARGV.first, "case"]
str.send((method << "!").join) if ARGC.to_i > 0
# => "FOO"
In this case, assuming I pass an argument from the command line, the transformation method will be computed and then applied to the string.
As you can guess, the only way in both cases to know that str
is going to be changed is by actually executing the code.
These examples should also answer your second part of the question
How could any code change such a string once the interpreter has started running? If we say that "abcd" needs to be frozen, then there must be some way of changing it.
As a side note, I'd like to point out that the "frozen string literals" feature has evolved recently and it's now sufficient to flag a variable as frozen
.
In Ruby 2.1,
"str".freeze
is optimized by the compiler to return a single shared frozen strings on every invocation. An alternative "str"f syntax was implemented initially, but later reverted.