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, theFEATURE_DEFAULTS
CONST is changed. If I do not usemerge!
inextract_features!
, and usemerge
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
Read Input from Console in Ruby
In Ruby, What Is the Cleanest Way of Obtaining the Index of the Largest Value in an Array
How to Drop to the Irb Prompt from a Running Script
Copying Gems from Previous Version of Ruby in Rbenv
Using Rails to Consume Web Services/Apis
Mongo - Ruby Connection Problem
Installing Rails: "File Not Found: Lib"
What Does the Ruby Method 'To_Sym' Do
How to Get Url of Active Storage Image
How Does Ruby Return Two Values
When Do We Use the "||=" Operator in Rails? What Is Its Significance
Finding Nil Has_One Associations in Where Query
Protecting the Content of Public/ in a Rails App
How to Install Ruby on Rails 3 on Osx