>> s = "hello"
=> "hello"
>> s[1] = '*'
=> "*"
>> s
=> "h*llo"
>> s2 = s.clone
=> "h*llo"
>> s2[0] = '!'
=> "!"
>> s2
=> "!*llo"
>> s1
>> s
=> "h*llo"
Notice that when we change the clone s2, it has no efect on s.Then I spent some time developing a program that constitutes a minor hit for the homework. I said I wanted to write a program that allows a user to guess a random one-digit odd number. I asked how many one-digit odd numbers there are and someone said 5. Ruby has a method rand(n) that returns a random value between 0 and n-1. So we can compute the guess by saying:
answer = 2 * rand(5) + 1
We have to pick a loop construct to use. I reminded people that there is a
great
summary of the control options in Ruby in the quick reference
guide linked under the Ruby tab on the course homepage.I mentioned that I personally like the loop/do/end construct that is used in conjuction with a break expression. This provides flexibility for any kind of loop because the break expression can be placed at the top of the loop, the bottom of the loop, or the middle of the loop. There can even be more than one break expression, although that can make your code difficult to understand.
We also want to give a short intro and we want to count the number of guesses. Putting all of this together, we ended up with the following complete program:
print "Try to guess an odd one-digit number.\n\n"
guesses = 0
loop do
print "Your guess? "
guess = gets.chomp
guesses += 1
break if answer == guess
end
print "You got it in ", guesses, " guesses\n"
Unfortunately, it didn't quite work. It never recognized that we had guessed
the right answer. This can happen often in a language like Ruby with dynamic
typing. It can be fun to avoid all of those type declarations that Java
requires and all of that type checking that OCaml performed, but that can lead
you to be complacent and to forget that types do matter. In this case, we are
calling the method gets which returns a string and then calling the chomp
method on that string which returns a new string. We then try to compare that
string to an int and they will never be equal to each other.We were able to fix this problem with a call on the to_i method of the string class that converts the string to an Integer:
guess = gets.chomp.to_i
When we ran this program, we found that it allowed us to enter values that
weren't positive integers. I modified the loop to call a method called prompt
that is part of the homework. It takes two strings as parameters, a prompt and
an error message, and it requires a block that includes error-checking code.
This replaced the previous prompt and read code in the loop:
loop do
guess = prompt("Your guess? ", "Enter a positive integer") {|str|
str.to_i > 0}.to_i
guesses += 1
break if answer == guess
end
It again didn't quite work because we repeated the same old mistake. The
prompt method returns a string, so we have to call to_i to convert it to an
Integer:
guess = prompt("Your guess? ", "Enter a positive integer") {|str|
str.to_i > 0}.to_i
This version worked as intended:
barb% ruby guess_odd.rb
Try to guess an odd one-digit number.
Your guess? foo
Enter a positive integer
Your guess? -3
Enter a positive integer
Your guess? 0
Enter a positive integer
Your guess? 1
Your guess? 7
Your guess? 9
Your guess? 5
Your guess? 3
You got it in 5 guesses
I then spent some time discussing the object-oriented features of Ruby. I
started with a simple Point class for storing x/y coordinates that was a
section problem:
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
def distance(other)
return Math.sqrt((@x - other.x) ** 2 + (@y - other.y) ** 2)
end
end
I mentioned that the to_s method is referring to instance variables @x and @y.
I changed these to simply x and y and it still worked. I pointed out that it
is calling the getter methods we requested with our call on attr_reader.I tried going in the other direction by changing the x and y in distance to be @x and @y:
def distance(other)
return Math.sqrt((@x - other.@x) ** 2 + (@y - other.@y) ** 2)
end
Ruby gave an error message when we tried to load this version of the code.
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
instance variables, there isn't a convenient way to refer to the instance
variables of another object, even an object of the same type. Although the
instance variables are called @x and @y, we can't refer to other.@x and
other.@y. So I put it back the way it was before:
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
We then spent some time developing code for a class called Tree that would
store a binary search tree of values. I said that we wanted a constructor, a
method called insert, and a method called print:
class Tree
def initialize
end
def insert(v)
end
def print
end
end
I asked how to implement this and someone said we need a node class. We don't
want a client to see the node class, so we used the keyword private to make
this a private inner class:
private
class Node
def initialize(data = nil, left = nil, right = nil)
@data = data
@left = left
@right = right
end
attr_reader :data, :left, :right
attr_writer :data, :left, :right
end
I asked what we need as instance variables and someone said that we need to
keep track of the overall root of the tree:
def initialize
@overall_root = nil
end
And what about insert? In 123 and 143 we talked about a public/private pair
for implementing binary tree operations because for our recursive method we
need an extra parameter to indicate the root of the tree. So we wrote this
helper method that we included in the private section of the class:
def insert_helper(v, root)
if root == nil
root = Node.new(v)
else if v < root.data
insert_helper(v, root.left)
else
insert_helper(v, root.right)
end
end
And we include a call on this passing the overall root in the public method:
def insert(v)
insert_helper(v, @overall_root)
end
We did something similar for printing, writing this private helper method:
def print_helper(root)
if root
print_helper(root.left)
puts root.data
print_helper(root.right)
end
end
And having our public method call the private method passing the overall root
as the place to start:
def print
print_helper(@overall_root)
end
Unfortunately, this version didn't work. The problem is the same problem we
have in Java. Ruby passes references as parameters, but as with Java, they are
passed using a value parameter mechanism. In other words, each method gets a
copy of any reference passed to it. So when the insert method changes its
parameter root, that has no effect on whatever was passed to root.The fix is the same as in Java. We turn this into the x=change(x) form. A complete version of the class is available from the calendar as tree1.txt.