Wednesday Jul 30, 2008

Ruby Eye for the Java guy: Constants

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 javax.xml.XMLConstants.

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[1] = "z"
  puts MyClass::MY_VAR           #=> "xzx"
Everything in Ruby is an object, and all objects inherit from the Object class. Object defines a freeze method, 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[1] = "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[1] = '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"
About

gjmurphy

Search

Top Tags
Archives
« July 2008
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
31
  
       
Today