CSE413 Notes for Friday, 3/1/24

We continued our discussion of blocks and the yield statement. We started with a simple method that yields four times:

        def f
          yield
          yield
          yield
          yield
        end
This method can be used to execute some bit of code four times, as in:

        >> f {puts "hello"}
        hello
        hello
        hello
        hello
        => nil
I said that I think the best way to think of this is that there are two bits of code that switch back and forth. When we call f, we start executing its code, but every time that f calls yield, we switch back to the code passed in the block and execute it. So this method switches back and forth four times.

It's more interesting when we yield a value:

        def f
          yield 43
          yield 79
          yield 19
          yield "hello"
        end
We can still pass simple code like before and it will execute four times:

        >> f {puts "hello"}
        hello
        hello
        hello
        hello
        => nil
But with this version, we have the option of writing a block that includes a parameter:

        >> f {|n| puts n * 3}
        129
        237
        57
        hellohellohello
        => nil
We can also have yield produce more than one result:

        def f
          yield 43, 17
          yield 79, 48
          yield 19, "bar"
          yield "hello", 39
        end
We can then execute a block that takes two parameters:

        >> f {|m, n| puts m; puts n; puts}
        43
        17
        
        79
        48
        
        19
        bar
        
        hello
        39
        
        => nil
In the example above I use semicolons to separate the three statements. Another way to do this is using the do..end form for a block:

        f do |m, n|
          puts m
          puts n
          puts
        end
We saw that we could even write f so that it sometimes yields one value and sometimes yields two values, as in:

        def f
          yield 43
          yield 79, 48
          yield 19, "bar"
          yield "hello"
        end
If we then write a block that takes two parameters, the second parameter will be set to nil when yield supplies just one value. We can test this to make sure that we do the right thing when the second parameter is nil, as in:

        f do |m, n|
          puts m
          puts n if n
          puts
        end
which produces this output:

        43
        
        79
        48
        
        19
        bar
        
        hello
        
        => nil
Notice that we don't have to say "if n == nil". In Ruby, nil evaluates to false and anything that is not either false or nil evaluates to true.

I again pointed out the idea that a block is a closure. For example, suppose you introduce these definitions into irb:

        def g(n)
          return 2 * n
        end
        
        a = 7
The variable a is a local variable and the method g is a method of the main object. And yet, we can refer to these in writing a block:

        >> x = MyRange.new(1, 5)
        => #<MyRange:0xb7ef74d4 @last=5, @first=1>
        >> x.each {|n| puts n + g(n) + a} 10 13 16 19 22 => nil 
The each method is in a separate class, so how does it get access to the local variable a and the method g? That works because in Ruby a block keeps track of the context in which it appears, giving you access to any local variables and remembering the value of "self" (the object you were talking to when you defined the block).

Then I asked people how we could implement a method that would be like the filter function in OCaml. The idea would be to pass a predicate as a block and to return a list of all values that satisfy the predicate. For example, we might ask for a list of all even numbers in a range by saying:

        x.filter {|n| n % 2 == 0}
We went to our class definition and introduced a new method header:

        def filter
I asked people how to do this and someone said we'd need to start with an empty list of values:

        def filter
          result = []
Then we have to go through every value in the range. In the each method we did that with a while loop that incremented a local variable. For this method we can use a for-each loop to keep things simple. But what does it loop over? It loops over the object itself. In Java we use "this" to refer to the object. In Ruby we use "self":

        def filter
          result = []
          for i in self
            ...
          end
And what do we want to do inside the loop? We want to test the value i to see if it satisfies our predicate. We do so with a call on yield passing it i. If it returns true, we add that value to the end of our list using the push method of the Array class:

        def filter
          result = []
          for i in self
            if yield(i) then
              result.push i
            end
          end
And what's left to do after that? We just have to return our result:

        return result
Some Ruby programs don't like to use "return". They simply list the value to return because a call on a Ruby method returns whatever the last expression evaluation returns:

        result
I tend to include the return, but mostly because I'm only a tourist in Rubyland and I'm more used to that syntax. Putting this all together, we ended up with the following complete MyRange class:

        class MyRange
          def initialize(first, last)
            @first = first
            @last = last
          end
        
            def first()
              return @first
            end

            def first=(n)
              @first = n
            end

            def last()
              return @last
            end

            def last=(n)
              @last = n
            end

          def each
            i = @first
            while i <= @last
              yield i
              i += 1
            end
          end
        
          def filter
            result = []
            for i in self
              if yield(i) then
                result.push i
              end
            end
            return result
          end
        end
It worked as expected when we tried to filter for even numbers or numbers divisible by 3:

        >> x = MyRange.new(1, 20)
        => #<MyRange:0xb7f0d7fc @last=20, @first=1>
        >> x.filter{|n| n % 2 == 0}
        => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
        >> x.filter{|n| n % 3 == 0}
        => [3, 6, 9, 12, 15, 18]
I then spent a few minutes talking about a few extra features of Ruby. Just as there is a "puts" method to write a line of output, there is a "gets" method that reads a line of input from the user:

        >> x = gets
        hello there
        => "hello there\n"
I think it's unfortunate that Ruby decided to include the newline characters as part of the string returned by gets. There is a standard Ruby method called chomp that can be used to eliminate newline characters:

        >> y = gets.chomp
        how are you?
        => "how are you?"
Then 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 ended by quickly showing that lists and range objects have a map method that is very similar to what we have seen in OCaml and Scheme. You provide a method to be applied to each value in the form of a block and it returns a new list that stores the result of applying the method to each value in the list or range, as in:

        >> (1..10).map {|n| 2 * n}
        => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
We will explore these higher order functions more in the next lecture.


Stuart Reges
Last modified: Sat Mar 2 10:02:29 PST 2024