CSE341 Notes for Monday, 5/20/24

I spent some time demonstrating the bagels program that you will complete for the homework. I mentioned the idea that you might want to change a string while you are computing what clues to give and you would want to change a copy. You can get a copy by calling the clone method, as in:
        >> 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.


Stuart Reges
Last modified: Mon May 20 14:40:45 PDT 2024