Ruby Eye for the Java guy: Constants
By gjmurphy on Jul 30, 2008
In Java, it is common to use public, final, static fields to represent utility constants. Since the fields are public, they can be used from any package; and since they are final, values cannot be assigned to them once they have been initialized. If the field is a primitive type, such as
java.lang.Integer.MAX_VALUE then it is also effectively immutable (and, by extension, effectively thread-safe). These two conditions - finality and immutability - are what guarantee that the field will remain constant. Some classes, like
java.lang.String, are also immutable, and so can be used to represent constants. A common design pattern is to group sets of related constants together in a public final class, such as for example
It would seem like the need for an API to expose constants is universal, and so should find expression in every language. How to do this in Ruby?
In Ruby, any field that begins with an upper-case letter is treated as constant. If you attempt to assign to a constant more than once, the interpreter will issue a warning. Constants may be defined inside of a class, and since the Ruby Class class inherits from Module, you can use the module "::" accessor syntax to read the constant's value:
class MyClass MY_VAR = "xxx" end puts MyClass::MY_VAR #=> "xxx" MyClass::MY_VAR = "yyy" #=> "warning: already initialized constant MY_VAR"However, the Ruby String class is mutable, so the constant is still open to modification:
MyClass::MY_VAR = "z" puts MyClass::MY_VAR #=> "xzx"Everything in Ruby is an object, and all objects inherit from the Object class. Object defines a
freezemethod, which, once invoked, prevents any further modification to the object's state. Once frozen, an object can never be "thawed". So a final, immutable class constant in Ruby might look like this:
class MyClass MY_VAR = "xxx" MY_VAR.freeze end puts MyClass::MY_VAR #=> "xxx" MyClass::MY_VAR = "yyy" #=> "warning: already initialized constant MY_VAR" MyClass::MY_VAR = "z" #=> "in '=': can't modify frozen string (TypeError)The attempt to modify the frozen object results in an unchecked error, and execution halts. Re-assigning to the constant results in a warning only, and execution continues. It is possible to make assignment a fatal error, too, if we wrap the constants in a module, and freeze the module:
module MyModule MY_VAR = "xxx" MY_VAR.freeze end MyConstants.freeze MyModule::MY_VAR = "YYY" #=> "can't modify frozen module (TypeError)Be careful, if you want to "mix in" the module's functionality, callers of your class will access the module's constants through an unfrozen path:
class MyClass include MyConstants end MyClass::VAR = "qqq" MyClass::VAR = 'r' puts MyClass::VAR #=> "qrq"One solution might be to freeze your class, if you want to guarantee the immutability of everything in the class. If you want to leave the class open to extension or modification, you could wrap the included constants in an inner module:
module MyConstants VAR = "xxx" VAR.freeze end MyConstants.freeze class MyClass class Constants include MyConstants end Constants.freeze end puts MyClass::Constants::VAR #=> "xxx"