3

I understand that "constants" in Ruby are by convention called constants but are in fact mutable. However I was under the impression that when they were "mutated" that there was a warning:

class Z2
 M = [0,1]
end

Z2::M # => [0, 1]
Z2::M = [0,3]
(irb):warning: already initialized constant Z2::M
(irb):warning: previous definition of M was here

However I found this is not the case all the time:

a = Z2::M
a[1] = 2

Z2::M # => [0,2] and no warning

Is this a gap in the "warning" system? I am inferring that assignment of a constant would duplicate it, but I guess that is not true either as it appears that constants and variables point to the same object? Does this mean that all so-called "constants" need to be frozen in order to prevent them from being changed without warning?

TJChambers
  • 1,489
  • 1
  • 18
  • 28
  • 1
    If you want to prevent alteration of that value, you might try `M = [0,1].freeze`. `Z2::M[1] = 2` => `RuntimeError: can't modify frozen array`. – Chris Heald Oct 23 '14 at 21:37
  • 1
    `freeze` will not work in all cases, as it only freezes the references, but not referenced objects. Example: `M=['hello'].freeze; M[0] << '!'` - no error, string modified. – BroiSatse Oct 23 '14 at 21:40
  • Well of course, the string isn't frozen :) – Chris Heald Oct 23 '14 at 21:42
  • This is getting more and more scary. Is there anyway to ensure that a constant's content are NEVER altered? I assumed freezing would mean it wasn't altered, but since everything is a reference, freezing it may not even prevent the reference from changing? – TJChambers Oct 23 '14 at 21:44
  • @TJChambers - you can wirte deep_freeze method which will recursively freeze all referenced objects. However - you don't really need it. I know it sounds weird, but you don't need constants in ruby as much as you need them in other languages and it is really hard to misuse them. What is your programming background? – BroiSatse Oct 23 '14 at 21:51
  • 3
    There's practically nothing that's guaranteed to be immutable in Ruby. You can get into anything and change it if you try hard enough. :) – Chris Heald Oct 23 '14 at 21:54
  • Ok - So bottomline is I need to quit thinking anything is a "constant". I have programmed in fluently 6 languages over 40 years, and Ruby only last 2. I am getting burnt by assigning "constants" to initialize variables to a starting state and then changing the variables, which "changes the constant" so the next variable that needs primed is not primed by the value in the source code. I was expecting some way to create the initial constant value I could reference throughout the code, but that seems fruitless because I get "hidden" constant corruption elsewhere. Thanks for your comments. – TJChambers Oct 23 '14 at 21:57
  • @BroiSatse `Object#freeze` literally does work in all cases, you just need to be aware what is the receiver, and that it won't *magically* propagate to other objects it holds the references to. Your example may be rewritten like `M=['hello'].collect(&:freeze).freeze; M[0] << '!' # RuntimeError: can't modify frozen String`. – David Unric Oct 23 '14 at 23:32
  • @TJChambers You'd probably first learn fundamentals of Ruby type system so things like every variable and constant is a reference to an object won't confuse you anymore. Yes, constants in Ruby are not immutable only an advice this reference should not be modified (thus the warning). See there is no special syntax or a keyword to define a constant, only 1st uppercase letter as a distinction. If a *constant* holds a non-constant reference, it shouldn't be surprising it can be modified without a warning. – David Unric Oct 24 '14 at 00:18

3 Answers3

8

TL;DR

Short of monkey-patching Kernel#warn (see https://stackoverflow.com/a/662436/1301972) to raise an exception, you won't be able to prevent reassignment to the constant itself. This is generally not a pragmatic concern in idiomatic Ruby code where one expects to be able to do things like reopen classes, even though class names are also constants.

A Ruby constant isn't actually immutable, and you can't freeze a variable. However, you can get an exception to be raised when something attempts to modify the contents of a frozen object referenced by the constant.

Freezing Objects Deeply with Plain Ruby

Freezing an Array is easy:

CONSTANT_ONE = %w[one two three].freeze

but the strings stored in this Array are really references to String objects. So, while you can't modify this Array, you can still (for example) modify the String object referenced by index 0. To solve this problem, you need to freeze not just the Array, but the objects it holds, too. For example:

CONSTANT = %w[one two three].map(&:freeze).freeze

CONSTANT[2] << 'four'
# RuntimeError: can't modify frozen String

CONSTANT << 'five'
# RuntimeError: can't modify frozen Array

Freezing Objects Recursively with a Gem

Since freezing recursive references can be a bit unwieldy, it's good to know there's a gem for that. You can use ice_nine to deep-freeze most objects:

require 'ice_nine'
require 'ice_nine/core_ext/object'

OTHER_CONST = %w[a b c]
OTHER_CONST.deep_freeze

OTHER_CONST << 'd'
# RuntimeError: can't modify frozen Array

OTHER_CONST[2] = 'z'
# RuntimeError: can't modify frozen Array

A Better Way to Use Ruby Constants

Another option to consider is calling Object#dup when assigning the value of a constant to another variable, such as instance variables in your class initializers, in order to ensure you don't mutate your constant's references by accident. For example:

class Foo
  CONSTANT = 'foo'
  attr_accessor :variable

  def initialize
    @variable = CONSTANT.dup
  end
end

foo = Foo.new
foo.variable << 'bar'
#=> "foobar"

Foo::CONSTANT
#=> "foo"
Community
  • 1
  • 1
Todd A. Jacobs
  • 81,402
  • 15
  • 141
  • 199
  • Thorough compilation - I appreciate it. I am much better informed for all the contributions to this question. – TJChambers Oct 24 '14 at 14:56
1

There is no gap, as you are not altering a constant. And the fact is that Ruby constants are just variables with extra warnings.

Constant, just as every variable, is merely a pointer to the object in memory. When you doM = [0,3] you are creating a new array and re-pointing constant to this new object, which triggers a warning.

However, when you run M[0] = 1 you are just modifying referenced object, but you do not change the constant, as it still points to the same object.

Important thing to realize here is that all classes in Ruby are just objects in memory, referenced with constants, so when you do:

class Z2
end

it is equivalent to (if Z2 is not defined or is not pointing onto a class object already):

Z2 = Class.new

Naturally class is a very dynamic object, as we keep adding methods to it and so on - we definitively don't want this to trigger any warnings.

BroiSatse
  • 44,031
  • 8
  • 61
  • 86
  • I understand some of that. Is it ok to say that the only warning I get is when I alter the object ID (aka pointer) and not when I alter the object (constant's) content?. It seems like the warning is about "initialization" and not "alteration of value". Is that a correct statement? – TJChambers Oct 23 '14 at 21:40
  • It is not how I would word it, but it has the similar meaning. Since constant points to the object, and the methods are send directly to the method, not touching the Constant, object have no idea it is supposed to be constant. Also, if you do `m = M`, you firstly expect that they both points to the same object, and hence they both would have to be constants, which would make absolutely no sense. – BroiSatse Oct 23 '14 at 21:48
0

If you do Z2::M[1] = 2 you won´t get the message either. I believe the lack of warning occours because you are changing the Array itself and not the reference Z2::M.
If M was an integer, for exemple:

class Z2
  M = 1
end

a = Z2::M
a = 2
a # => 2
Z2::M # => 1

To modify an Array from a constant without modify the original you can do:

class Z2
  M = [0,1]
end

a = Z2::M.dup
a[0] = 1
a # => [1,1]
Z2::M # => [0,1]
Doguita
  • 15,403
  • 3
  • 27
  • 36