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 ML. 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 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?"
You can define a variable that is tied to an external input file by calling
File.open:
>> infile = File.open("utility.sml")
=> #<File:utility.sml>
Here I'm reading our file of ML utility functions that we used earlier in the
quarter. To get a line of text, you can call gets on this object, as in:
>> infile.gets
=> "(* Stuart Reges *)\n"
>> infile.gets
=> "(* 1/17/07 *)\n"
>> 3.times {puts infile.gets}
(* *)
(* Collection of utility functions *)
=> 3
You can also use a for-each loop, as in:
for str in infile
puts str
puts str
end
This will echo each line of the input file twice. Or you can read the whole
thing into an array by saying:
lines = infile.readlines
Keep in mind, though, that the file object keeps track of where it is in the
file, so you might need to open the file again to read it more than once.I then spent the last few minutes of class showing people the Bagels and Jotto programs that are included in homework 8.