Throw Exception When Re-Assigning a Constant in Ruby

Throw exception when re-assigning a constant in Ruby?

Look at Can you ask ruby to treat warnings as errors? to see how it is possible in some cases to treat warnings as errors.

Otherwise I guess you'd have to write a custom method to assign constants and raise the exception if already assigned.

If you know that a reassignment happens to a specific constant, you can also add a sanity check just before the assignment.

Ruby assign a variable or raise error if nil


Basically, what I want to do is this

target_url = @maybe_a || @maybe_b || raise "either a or b must be assigned"

You have to add parentheses to raise to make your code work:

x = a || b || raise("either a or b must be assigned")

It would be "more correct" to use the control-flow operator or instead of ||: (which makes the parentheses optional)

x = a || b or raise "either a or b must be assigned"

This is Perl's "do this or die" idiom which I think is clean and neat. It emphases the fact that raise doesn't provide a result for x – it's called solely for its side effect.

However, some argue that or / and are confusing and shouldn't be used at all. (see rubystyle.guide/#no-and-or-or)

A pattern heavily used by Rails is to have two methods, one without ! which doesn't provide error handling:

def a_or_b
@maybe_a || @maybe_b
end

and one with ! which does:

def a_or_b!
a_or_b || raise("either a or b must be assigned")
end

Then call it via:

target_url = a_or_b!

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.

Assigning a variable the length of a string or array causes a dynamic constant assignment error in Ruby

Turns out that if the 1st letter of a variable is capitalized, it's treated as a constant. "L" should be changed to "l".

Constant Assignment Bug in Ruby?


Catch that? The constant was appended to at the same time the local variable was.

No, it wasn't appended to, and neither was the local variable.

The single object that both the constant and the local variable are referring to was appended to, but neither the constant nor the local variable was changed. You cannot modify or change a variable or constant in Ruby (at least not in the way that your question implies), the only thing you can change is objects.

The only two things you can do with variables or constants is dereferencing them and assigning to them.

The constant is a red herring here, it is completely irrelevant to the example given. The only thing that is relevant is that there is only one single object in the entire example. That single object is accessible under two different names. If the object changes, then the object changes. Period. It does not mysteriously split itself in two. Which name you use to look at the changed object doesn't matter. There is only one object anyway.

This works exactly the same as in any other programming language: if you have multiple references to a mutable object in, say, Python, Java, C#, C++, C, Lisp, Smalltalk, JavaScript, PHP, Perl or whatever, then any change to that object will be visible no matter what reference is used, even if some of those references are final or const or whatever that particular language calls it.

This is simply how shared mutable state works and is just one of the many reasons why shared mutable state is bad.

In Ruby, you can generally call the freeze method on any object to make it immutable. However, again, you are modifying the object here, so anybody else who has a reference to that object will all the sudden find that the object has become immutable. Therefore, just to be safe, you need to copy the object first, by calling dup. But of course, that's not enough either, if you think of an array, for example: if you dup the array, you get a different array, but the objects inside the array are the still the same ones in the original array. And if you freeze the array, then you can no longer modify the array, but the objects in the array may very well still be mutable:

ORIG = ['Hello']
CLONE = ORIG.dup.freeze
CLONE[0] << ', World!'
CLONE # => ['Hello, World!']

That's shared mutable state for you. The only way to escape this madness is either to give up shared state (e.g. Actor Programming: if nobody else can see it, then it doesn't matter how often or when it changes) or mutable state (i.e. Functional Programming: if it never changes, then it doesn't matter how many others see it).

The fact that one of the two variables in the original example is actually a constant is completely irrelevant to the problem. There two main differences between a variable and a constant in Ruby: they have different lookup rules, and constants generate a warning if they are assigned to more than once. But in this example, the lookup rules are irrelevant and the constant is assigned to only once, so there really is no difference between a variable and a constant in this case.

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"

Modifying a constant for a Subclass

Constants are name spaced within the class or module they are defined. They are resolved through the usual ancestors path. In your subclass you can define a constant of the same name as one in the superclass, and the expression initializing it can reference the superclass's constant as the subclass's constant won't be defined until after the initial assignment. Like this:

$ pry
[1] pry(main)> class A; Items = [[1, 3, 5], [2, 4, 6]]; end
=> [[1, 3, 5], [2, 4, 6]]
[2] pry(main)> class B < A; end
=> nil
[3] pry(main)> class B; Items; end
=> [[1, 3, 5], [2, 4, 6]]
[4] pry(main)> A::Items
=> [[1, 3, 5], [2, 4, 6]]
[5] pry(main)> B::Items
=> [[1, 3, 5], [2, 4, 6]]
[6] pry(main)> class B; Items = Items.dup << [7,8,9]; end
=> [[1, 3, 5], [2, 4, 6], [7, 8, 9]]
[7] pry(main)> A::Items
=> [[1, 3, 5], [2, 4, 6]]
[8] pry(main)> B::Items
=> [[1, 3, 5], [2, 4, 6], [7, 8, 9]]

When deriving the new constant, be careful to dup the original if you plan to modify it with a mutating method (like Array#<<). See the trap:

[9] pry(main)> class A; Foo = [[1,2],[3,4]]; end
=> [[1, 2], [3, 4]]
[10] pry(main)> A::Foo
=> [[1, 2], [3, 4]]
[11] pry(main)> class B; Foo = Foo << [5,6]; end
=> [[1, 2], [3, 4], [5, 6]]
[12] pry(main)> B::Foo
=> [[1, 2], [3, 4], [5, 6]]
[13] pry(main)> A::Foo
=> [[1, 2], [3, 4], [5, 6]]
[14] pry(main)> B::Foo.object_id == A::Foo.object_id
=> true
[15] pry(main)> B::Items.object_id == A::Items.object_id
=> false

You can explicitly reference the constant in the parent namespace without naming the superclass using Class#superclass

[16] pry(main)> class B; superclass::Items; end
=> [[1, 3, 5], [2, 4, 6]]

Unitialized constant when using custom exceptions

The problem here is that lib/ files are autoloaded (config.autoload_paths).
Autoload depends on filenames in order to find needed class.
The file named bandcamp.rb will be loaded only when you call Social::Bandcamp and only then you'll be able to access other classes defined there.

The solution is to create separate files for exceptions or turn on eager loading of this directory.



Related Topics



Leave a reply



Submit