How to Redefine a Ruby Constant Without Warning

How to redefine a Ruby constant without warning?

The following module may do what you want. If not it may provide some pointers to your solution

module RemovableConstants

def def_if_not_defined(const, value)
self.class.const_set(const, value) unless self.class.const_defined?(const)
end

def redef_without_warning(const, value)
self.class.send(:remove_const, const) if self.class.const_defined?(const)
self.class.const_set(const, value)
end
end

And as an example of using it

class A
include RemovableConstants

def initialize
def_if_not_defined("Foo", "ABC")
def_if_not_defined("Bar", "DEF")
end

def show_constants
puts "Foo is #{Foo}"
puts "Bar is #{Bar}"
end

def reload
redef_without_warning("Foo", "GHI")
redef_without_warning("Bar", "JKL")
end

end

a = A.new
a.show_constants
a.reload
a.show_constants

Gives the following output

Foo is ABC
Bar is DEF
Foo is GHI
Bar is JKL

Forgive me if i've broken any ruby taboos here as I am still getting my head around some of the Module:Class:Eigenclass structure within Ruby

How to disable warning for redefining a constant when loading a file

To suppress warnings, use the following code at the top of the script:

$VERBOSE = nil

Ruby CONSTANTS seem to be INVISIBLY ALTERABLE?

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"

Ruby - Forbid constants redefinition

Just scope your constant within a module and then use Module#freeze to prohibit further modification of the module.

E.g.

module Really
CONSTANT = :foo
freeze
end

Really::CONSTANT = :bar
-> RuntimeError: can't modify frozen Module

Note that this doesn't speak to the mutability of the value assigned to the constant. For that, use Object#freeze.

This approach will bite you hard in environments where your code is reloaded, like in a Rails application. There, you'll have to jump through an additional hoop, checking whether the module has been defined yet before defining it.

Generally, defensive programming in Ruby is more trouble than it's worth. What's your concern?

Set a Ruby variable and never be able to change it again?

They are called constants. A constant in Ruby is defined by a UPPER_CASE name.

VARIABLE = "foo"

It is worth to mention that, technically, in Ruby there is no way to prevent a variable to be changed. In fact, if you try to re-assign a value to a constant you will get a warning, not an error.

➜  ~  irb
2.1.5 :001 > VARIABLE = "foo"
=> "foo"
2.1.5 :002 > VARIABLE = "bar"
(irb):2: warning: already initialized constant VARIABLE
(irb):1: warning: previous definition of VARIABLE was here
=> "bar"

It's also worth to note that using constants will warn you if you try to replace the value of the constant, but not if you change the constant value in place.

2.1.5 :001 > VARIABLE = "foo"
=> "foo"
2.1.5 :002 > VARIABLE.upcase!
=> "FOO"
2.1.5 :003 > VARIABLE
=> "FOO"

In order to prevent changes to the value referenced by the constant, you can freeze the value once assigned.

2.1.5 :001 > VARIABLE = "foo".freeze
=> "foo"
2.1.5 :002 > VARIABLE.upcase!
RuntimeError: can't modify frozen String
from (irb):2:in `upcase!'
from (irb):2
from /Users/weppos/.rvm/rubies/ruby-2.1.5/bin/irb:11:in `<main>'
2.1.5 :003 > VARIABLE
=> "foo"

Here's an example inside a class.

class MyClass
MY_CONSTANT = "foo"
end

MyClass::MY_CONSTANT
# => "foo"

how to get rid of ruby's warning: already initialized constant

The easy way:

v, $VERBOSE = $VERBOSE, nil
# code goes here
$VERBOSE = v

Override ruby constant in subclass so inherited methods use new constant instead of the old?

I've done this by simply redefining the constant in the subclass, and then referring to it in methods as self.class::CONST in instance methods and self::CONST in class methods. In your example:

class SuperClass
CONST = "Hello, world!"
def self.say_hello
self::CONST
end
end

class SubClass < SuperClass
CONST = "Hello, Bob!"
end

SubClass.say_hello #=> "Hello, Bob!"

Right way to check if a constant is already defined in a Ruby Class

The keyword defined? is documented here.

It is better to ask if it is a constant, and use const_defined? if it is important that it is a constant. If you only care that it is defined, then use the keyword defined?

Can `merge!` change a CONST in Ruby?

I have a situation where merge! seems to modify the value of a CONST. Can this occur? How?

No, in general, methods cannot change variable bindings, regardless of whether those variables are local variables, instance variables, class variables, global variables, or constants. Variables aren't objects in Ruby, you can't call methods on them, you can't pass them as arguments to methods, ergo, you can't tell them to change themselves.

The exception to this are meta-programming methods like Binding#local_variable_set, Object#instance_variable_set, Module#class_variable_set, or Module#const_set.

What you can do, however, and what you are doing in this case, is tell the object the variable points to to change itself. The documentation for Hash#merge! is unfortunately a bit unclear in that it does not explicitly mention that Hash#merge! mutates its receiver.

This seems to confirm that, after running the extract_features! method, the FEATURE_DEFAULTS CONST is changed. If I do not use merge! in extract_features!, and use merge instead, then the CONST value does not change.

No, it only confirms that the state of the object FEATURE_DEFAULTS points to changed, it doesn't say anything about whether the binding of FEATURE_DEFAULTS, i.e. which object it points to, changed. You can confirm that the constant still points to the same object by looking at its object_id.

Of course, constants can be re-assigned anyway, albeit triggering a warning.

Why can I change constants?

Well, constants in Ruby are relatively variable. Objects they point to can be swapped (as in your example) and their state can be changed as well.

class TestClass
Constant = []
end
TestClass::Constant << "no warning at all!"

The only advantage they provide are warnings generated when you make an existing constant point to another object. See "Programming Ruby", section "Variables and Constants". It's old but still valid.

The purpose for Ruby's constants to exist is signalling that a given reference shouldn't be changed. For instance, if you do Math::PI = 3.0 you deserve to be warned.

Theoretically, you could break compatibility with the original implementation and enforce constants' immutability. As a result you could achieve a slight performance improvement thanks to optimised method dispatching.

In the example above you'd know that Constant.is_a? Array so dispatching the << symbol to the actual method could be done only once, on the first execution of that line. The problem is that Ruby enables you to redefine Array#<< thus making the problem more tricky.

Checking whether various Ruby implementations try to use such optimisation would require some additional research and digging in their documentation or sources.



Related Topics



Leave a reply



Submit