>> x = [1, 42, 7, 19, 8, 25, 12]
=> [1, 42, 7, 19, 8, 25, 12]
>> x.map {|n| 2 * n}
=> [2, 84, 14, 38, 16, 50, 24]
There is a find function that expects a block that specifies a predicate:
>> x.find {|n| n % 3 == 1}
=> 1
This version finds just the first occurrence. If you want to find them all,
you can call find_all which is Ruby's version of filter:
>> x.find_all {|n| n % 3 == 1}
=> [1, 7, 19, 25]
Ruby also has methods for determining whether every value satisfies a certain
predicate and whether all values satisfy a certain predicate:
>> x.any? {|n| n % 3 == 1}
=> true
>> x.all? {|n| n % 3 == 1}
=> false
These are computational equivalents of the mathematical existential quantifier
("there exists") and universal quantifier ("for all").Then I discussed the inject method. When you don't supply a parameter, it behaves like the reduce function in OCaml (collapsing a sequence of values into one value of the same type):
>> [3, 5, 12].inject {|a, b| a + b}
=> 20
But you can also call it with a parameter, in which case it behaves like
foldl:
>> [3, 5, 12].inject("values:") {|a, b| a + " " + b.to_s}
=> "values: 3 5 12"
It's nice that Ruby has the inject function for other types as well like
ranges:
>> (1..20).inject("values:") {|a, b| a + " " + b.to_s}
=> "values: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20"
Then we reviewed console and file-reading operations. I mentioned that I
particularly like the readlines method, as in:
lst = File.open("hamlet.txt").readlines
This read in the entire contents of Hamlet into an array of strings. We
were then able to ask questions like how many lines there are in the file or
what the 101st line is:
irb(main):002:0> lst.length
=> 4463
irb(main):003:0> lst[100]
=> " Hor. Well, sit we down,\r\n"
I asked people how we could write code to count the number of occurrences of
various words in the file. We'd want to split each line using whitespace,
which you can get by calling the string split method, as in:
irb(main):004:0> lst[100].split
=> ["Hor.", "Well,", "sit", "we", "down,"]
To store the counts for each word, we need some kind of data structure. In
Java we'd use a Map to associate words with counts. We can do that in Ruby
with a hashtable:
irb(main):005:0> count = Hash.new
=> {}
As we saw in an earlier lecture, we can use the square bracket notation to
refer to the elements of the table. For example, to increment the count for
the word "hamlet", we're going to want to execute a statement like this:
count["hamlet"] += 1
Unfortunately, when we tried this out, it generated an error:
irb(main):006:0> count["hamlet"] += 1
NoMethodError: undefined method `+' for nil:NilClass
from (irb):6
from :0
That's because there is no entry in the table for "hamlet". But Ruby allows us
to specify a default value for table entries that gets around this:
irb(main):007:0> count = Hash.new 0
=> {}
irb(main):008:0> count["hamlet"] += 1
=> 1
irb(main):009:0> count
=> {"hamlet"=>1}
Using this approach, it was very easy to count the occurrences of the various
words in the lst array:
irb(main):007:0> count = Hash.new 0
=> {}
irb(main):010:0> for line in lst do
irb(main):011:1* for word in line.split do
irb(main):012:2* count[word.downcase] += 1
irb(main):013:2> end
irb(main):014:1> end
After doing this, we could ask for the number of words in the file and the
count for individual words like "hamlet":
irb(main):022:0> count.length
=> 7234
irb(main):023:0> count["hamlet"]
=> 28
The File object can be used with a foreach loop, so we could have written this
same code without setting up the array called lst:
irb(main):024:0> count = Hash.new 0
=> {}
irb(main):025:0> for line in File.open("hamlet.txt") do
irb(main):026:1* for word in line.split do
irb(main):027:2* count[word.downcase] += 1
irb(main):028:2> end
irb(main):029:1> end
=> #<File:hamlet.txt>
irb(main):030:0> count.length
=> 7234
irb(main):031:0> count["hamlet"]
=> 28
The key point here is that it is possible to write just a few lines of Ruby
code to express a fairly complex operation to be performed. We'd expect no
less from a popular scripting language.I then spent some time discussing the object-oriented features of Ruby. I started with a simple Point class for storing x/y coordinates.
class Point
def initialize (x = 0, y = 0)
@x = x
@y = y
end
attr_reader :x, :y
attr_writer :x, :y
def to_s
return "(#{@x}, #{@y})"
end
end
This class has several new features that I described. In the
constructor (the initialize method) the parameters have default values
of 0, which means there really are three constructors.We discussed in a prior lecture how to define getter and setter methods. Ruby has a special form for this called "attr_reader" and "attr_writer". In the class above, we put a colon in front of x and y to turn them into a symbol. These lines of code introduce a getter for each and a setter for each.
Finally, I am using another bit of syntatic sugar for the to_s method. Suppose you define a couple of variables called x and y:
irb(main):001:0> x = 3
=> 3
irb(main):002:0> y = 4.7
=> 4.7
In Java, if you wanted to print out the values of these variables, you
would say something like:
System.out.println("x = " + x + ", y = " + y);
You can do something equivalent in Ruby, but we would have to call
to_s for each of x and y to convert them to a string:
irb(main):004:0> puts "x = " + x.to_s + ", y = " + y.to_s
x = 3, y = 4.7
=> nil
Ruby gives an alternative where you embed an expression in #{...}
inside a quoted string. Ruby will evaluate the expression and convert
it to a string, as in:
irb(main):006:0> puts "x = #{x}, y = #{y}
irb(main):007:0" "
x = 3, y = 4.7
=> nil
irb(main):008:0> puts "sum = #{x + y}"
sum = 7.7
=> nil
Notice in the second case that it has to add x and y together before
converting it to a string.So back to our Point class. Fields like @x and @y are encapsulated, which basically makes them private. But Ruby has a different notion of "private" than Java does. In the case of fields, there isn't a convenient way to refer to the fields of another object, even an object of the same type. For example, we tried to write this distance method for the Point class:
def distance(other)
return Math.sqrt((@x - other.@x) ** 2 + (@y - other.@y) ** 2)
end
Ruby rejected this as syntactically invalid. Although the fields are called @x
and @y, we can't refer to other.@x and other.@y. We can only refer to fields
of "self" in that manner. But we found that this version worked:
def distance(other)
return Math.sqrt((@x - other.x) ** 2 + (@y - other.y) ** 2)
end
In this case, we are calling the "getter" function of the other Point object.
So other.x and other.y are really function calls, as in other.x() and
other.y(). Then I moved the attr_reader and attr_writer calls into a private
section of the class:
class Point
...
private
attr_reader :x, :y
attr_writer :x, :y
end
This broke the distance method. When we called it, we got an error message
about trying to call a private method. In Java, private means private to the
class. In Ruby, private means private to the object. In other words, the only
way to call a private method is in the context of self calling it. Even other
instances of the same class can't call a private method.These are the two extremes. A public method can be called by anyone. A private method can only be called by self. There is a third option. We can declare a method to be protected, in which case it can be called by objects of the same class and objects whose type is a subclass of this type. The calls still have to appear in the class definition, so that clients of the class aren't able to call the method. So changing "private" to "protected" in the example above allowed us to have a functioning distance method without exposing the x and y getters and setters outside the class.
I then pointed out an interesting property of instance variables. I added this new method to the class that mentions an instance variable @z:
def foo
@z = 26
end
When I then constructed a point, we saw that @z was not there:
>> p = Point.new 3, 15
=> #<Point:0xb7b692e0 @y=15, @x=3>
>> p.instance_variables
=> ["@y", "@x"]
But as soon as I called the foo method, the instance variable appears:
>> p.foo
=> 26
>> p
=> #<Point:0xb7b692e0 @z=26, @y=15, @x=3>
>> p.instance_variables
=> ["@z", "@y", "@x"]
This is very different from Java. Java is statically typed, so before the
program ever begins executing, we have to specify exactly what each object will
look like. So when Java constructs an object, it makes all of the fields at
the same time. But Ruby is far more dynamic. Instance variables are added to
the object as they are encountered in executing methods of the class. The @x
and @y instance variables are mentioned in the constructor, so they are
allocated when the constructor is called. The @z instance variable is only
mentioned in the foo method, so it is allocated only when we call foo.I then showed a quick example of inheritance:
class Point2 < Point
def translate(dx, dy)
self.x += dx
self.y += dy
end
end
The notation "Point2 < Point" in the header indicates that Point2 extends
(inherits from) Point. This subclass adds a translate method. By referring to
"self.x" and "self.y", this code is calling the superclass getters and setters
for x and y. This is the "right" way to go to preserve encapsulation. But
Ruby would allow us to directly access the instance variables if we wanted
to:
class Point2 < Point
def translate(dx, dy)
@x += dx
@y += dy
end
end
This wouldn't be allowed in Java where we tend to make instance variables
private, but in Ruby the philosophy is that subclasses should have access to
the superclass instance variables.I pointed that the keyword "super" is used differently in Ruby than in Java. In Java we say things like "super.to_s" to refer to the superclass version of the to_s method. In Ruby, "super" refers to the superclass version of whatever method you are overriding. So if you are overriding "to_s", then "super" refers to the superclass version of the method. For example, we added this method to the Point class to have the to_s method add an exclamation point:
def to_s
super + "!"
end
I then discussed the idea of writing an iterator for a binary tree class. How
would we implement a binary tree inorder iterator for a Java binary tree?
There were several suggestions. One idea was to do the complete traversal in
advance and store the result in some kind of data structure like an ArrayList
and then we could iterate over the ArrayList. Another suggestion was to keep a
stack and to simulate the call stack ourselves. That could work as well,
although that is also rather tricky.What Java does is to keep track of parent links in the tree and then you move around in the tree from node to node. That works, but it requires keeping extra parent links and the code to move from node to node is a bit tricky. Here is a bit of source code from TreeMap.java that has the code that moves from one node to the next using an inorder traversal:
private Entry<K,V> successor(Entry<K,V> t) {
if (t == null)
return null;
else if (t.right != null) {
Entry<K,V> p = t.right;
while (p.left != null)
p = p.left;
return p;
} else {
Entry<K,V> p = t.parent;
Entry<K,V> ch = t;
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
All of these solutions work, but none of them is simple and efficient. Ruby
gives us a solution that is simple and efficient. At that point we ran out of
time, so I said that we'd complete it in the next lecture.