Introduction to Ruby II: Classes
Concepts
Classes in Ruby are similar to classes in other languages like Python,Java, Smalltalk, and C++. Here is a quick review/tour of object-oriented terminology: objects are also known as instances. The methods which the object operates on are called instance methods in Ruby, member functions in C++, and messages in Objective-C. Class variables in Ruby are called static variables in C++ and Java. Class variables are "global" to the class. For example, in class Wave below,@@num_instances holds its value independent of whether an individual Wave object's memory has been deallocated.
Some conventions and rules
You don't have to pass in self to each instance method of the class. self, which is a reference to the current object, is an implicit parameter. The programmer has to explicitly specify self only in some instances, like when he/she is extending an existing class(see class Array below). Instance variables have to be prefixed by an @ symbol and class variables have to be prefixed by @@. You can find a handy table for variable naming rules here. To summarize the rest of the rules, local variables have to be lowercase or can start with an underscore and be uppercase, globals have to be prefixed by $, and constants and class names have to start with an uppercase letter.
Ruby classes are always "open", similar to ObjC. Here is an example which adds averaging to the Array class.
#!/usr/bin/env ruby -w
class Array
public
def avg
#I used to_f("to float") to use floating point division
#instead of integer division
(self.inject(0){|sum,i| sum + i}).to_f/self.length.to_f
end
end
if __FILE__ == $0
a = [1,2,3,4]
print a.avg
end
One does not have to write getter and setter methods for each instance variable.You can specify public and private like in C++, although by default, instance variables are private and methods are public. To do this you have three convenience methods: attr_writer, attr_reader, and attr_accessor. The first two make the instance variables write-only and read-only, while the last one is both read and write. The arguments to these convenience methods are symbols which is how the Ruby interpreter sees identifiers(class names, method names, etc.). However, just think of the : as connoting the phrase "the thing named". So :data translates to "the thing named data".
Symbols turn up everywhere in Ruby(a prime example would be Interface Builder outlets in RubyCocoa). Finally,inject above is useful for writing accumulation methods. The parameter to inject is the initial accumulator value. self.inject(0){|sum,i| sum + i}, which going through each element, adds sum, the accumulator to the ith element of the array. The return value of the block in each iteration is sum+i
An Example Class
This example class is useful for explaining many Ruby programming concepts. This class is a fragment of the code I converted from the Python code found in the article Waves and Harmonics by Chris Meyers. I will display the code first.
#!/usr/bin/env ruby
require 'tk'#not used in this code fragment
class Wave
@@num_instances = 0
attr_reader :data
def initialize(formula,points = 300)
@@num_instances += 1
@data = [] #empty array
points.times {|i| @data << 0.0}
@points = points
if(formula)
(0..points-1).each do |p|
x = p*Math::PI*2.0/points
#Math::PI indicates PI can be
#found in the Math module.
@data[p] = eval(formula)
end
end
end
def to_s
@data.each {|d| puts d}
#Look at each 'data point' in @data and output it
end
def +(other)
target = Wave.new(nil,@points)
#Go through each point, and do an elementwise add
target.data.each_index {|i| target.data[i] = @data[i] + other.data[i]}
target
end
def -(other)
target = Wave.new(nil,@points)
target.data.each_index {|i| target.data[i] = @data[i] - other.data[i]}
target
end
def Wave.getnumallocations
@@num_instances
end
# For the full code , please refer to the source code file named plotone.rb
end
Annotation
- Notice that I'm specifying that getter and setter methods for
datamust be automatically generated by typingattr_accessor :data. If, for some reason, you need to code the getter or setter explicitly, the method signature for setter would bedef data=(value)and the form for the getter would bedef data def initialize(formula,points = 300). This is the method that initializes the instance variables of the class. Here we are using a default value of 300 for the points parameter. In the method body, we either set all the data values to 0.0 or set the data based on the formula passed in: see the selection statementif(formula)above.to_s: Any class can implementto_sin order to letputsbe able to print an appropriate string representation of the class.def +(other)"overloads" the+operator for theWave.+is just like any other method, except it allows for syntactic sugar such asc = a + blike C++.- I have added a class variable that keeps track of the number of objects of the
Wavethat are allocated. In general, class variables hold data that is independent of any one instance of a class. Another example of a class variable would be the hash called@@basecomplementused in the simple bioinformatics script included with the rest of the source code. Note that class variables are not accessed from methods associated with an object a.k.a instance of the class, but from class methods such asdef Wave.getnumallocationsabove. - To invoke the class method like the one in the code, you just do
numinvoked = Wave.getnumallocations
Inheritance
All object-oriented languages support the concept of inheritance, characterized by the "IS-A" relationship. For example, a chocolate cake IS-A cake, an NSButton IS-AN NSView, etc. Let us look at an example of inheritance in Ruby. The less-than symbols mean "inherits from".
class BinaryOp
def initialize(left,right)
@left = left
@right = right
end
def compute
end
end
class Addition < BinaryOp
def compute
target = Wave.new(nil,@left.points)
(0...@left.points).each {|idx| target.data[idx] = @left.data[idx] + @right.data[idx] }
target
end
end
class Subtraction < BinaryOp
def compute
target = Wave.new(nil,@left.points)
(0...@left.points).each {|idx| target.data[idx] = @left.data[idx] - @right.data[idx] }
target
end
end
class Multiplication < BinaryOp
def compute
target = Wave.new(nil,@left.points)
(0...@left.points).each {|idx| target.data[idx] = @left.data[idx] + @right.data[idx] }
target
end
end
So we could have implemented memberwise subtract as follows:
def -(other)
x = Subtraction.new(self,other)
x.compute
end
The other functions, such as def +(other) follow the same pattern. Please see the source code file named plottwo.rb for the full change.
Blocks Continued
Last time I talked about how we could use blocks to iterate over the contents ofthe standard containers(array, hash,etc.) in Ruby. You probably observed that Ruby's iterators are internal to the container class or regular class;there is no built-in "iterator class" like in C++. There are a few cases in which external iterators are indispensable. For these cases, the Generator library provides external iterators.
What if we want our own classes and containers to support each, collect, etc.? All we have to do is mixin the module Enumerable. Ruby does not support multiple inheritance like Java and ObjC, but it does provide the mixin facility as an alternative. When you mixin a module, all the module's instance methods are available to your class. In effect, mixed-in modules act like superclasses. However, the difference between a module and a class is that a module can't have instances(objects) that uses its methods because it is not a class. However, when you mixin a module, your class's object can use all the "instance" methods of the module(from the book Programming Ruby). If you know about Java interfaces or ObjC protocols, modules are similar, but they are already defined before any class uses them in(mixes them in).
Implementing Each
What we have to do to get the class to support each, collect, etc. is to mixin the Enumerable module and provide a definition of each. Extending the Wave example,
class Wave
include Enumerable
#intermediate code omitted here
def each
@data.each {|pt| yield pt}
end
What yield pt does is pass pt to the block that each was invoked with. To make the code more robust, we can create each so that it warns the user if he/she forgets to pass in a block(I will talk about exceptions at a later time).
def each
#will talk about exception handling later
raise LocalJumpError,caller unless block_given?
@data.each {|pt| yield pt}
end
Our class can use collect and inject even though we didn't define them--we get them for free.
Until Next Time
A little exercise
What would you change about the inheritance approach? Hint: Why does class Multiplication only handle multiplication by another vector?
Running the code
When you run the code provided, you should see a primitive plot like this:

Next time, I will discuss unit testing, regular expressions,some mathematical libraries, and some other important features of the language. I'll talk about RubyCocoa in the tutorials after that.