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"

Thursday Apr 26, 2007

Ruby script to toggle CVS root in a workspace

Sandip Chitale posted a Java program that toggles the CVS root setting of a workspace that is already checked out (see A simple program to toggle CVSROOT of existing checked out workspace). At the end of his post, Sandip solicits alternative solutions. I have been playing around with Ruby, and was curious as to how much more compactly the same logic could be expressed in Ruby.

Stripped of empty lines and comments, Sandip's Java solution weighs in at 83 lines. That's roughly four times the 16 lines required for a roughly equivalent program in Ruby, which I give below.

The Java program has explicit exception handlers, which I chose to leave out, since both programs will leave the repository in a corrupted state if IO fails midway.

# A simple program to fix the CVSROOT.  
# 
# One may check out a cvs workspace using a CVSROOT e.g.
# :pserver:username@extranet.foo.com:/cvs. This value of CVSROOT 
# is recorded in the CVS/Root file in the checkedout directories.
# At some later time, depending on the network connectivity, it may be faster
# to use an alternate (aliased) CVSROOT e.g. 
# :pserver:username@intranet.foo.com:/cvs.
#
# This simple program provides the functionality to fix the CVSROOT settings
# in each CVS/Root file. It skips entries found in .cvsignore.
#
# CAUTION: Care must be taken to make sure that both cvs servers are really the
# same servers, or mirrors that are in sync.
#
# DISCLAIMER: This is an experimental program. Use at your own risk.

require 'find'
abort 'No server name specified' unless $\*[0]

roots = []
ignore_patterns = []


# Use the find module to traverse the source code repository, collecting all CVS root
# files found, and patterns of directories to ignore

Find.find('./') do |path|
  if path =~ /CVS\\/Root$/
    roots << path
  elsif path =~ /.cvsignore$/
    ignores = IO.readlines(path).collect{ |line| line.chop }
    ignore_patterns << File.dirname(path) + '/(' + ignores.join('|') + ')'
  end
end


# Construct a regexp of all directories to ignore, use it to prune the array of root
# files, then, update each root file with the new pserver name

ignore_expr = Regexp.new('\^(' + ignore_patterns.join('|') + ')')
roots.reject { |root| ignore_expr.match(root) ? true : false }.each do |root|
  (pserver = File.open(root, 'r').gets)[/@[\^:]+:/] = ('@' + $\*[0] + ':')
  File.open(root, 'w').puts pserver
end 
About

gjmurphy

Search

Top Tags
Archives
« April 2014
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
30
   
       
Today