irb(main):056:0> 3.times LocalJumpError: no block given from (irb):56:in `times' from (irb):56 from :0Using the curly brace notation we'd say:
irb(main):057:0> 3.times { puts "hello" } hello hello hello => 3The FixNum object executes the block of code the given number of times (3 times in this case because we asked 3 to do this task). We could instead use do...end notation:
irb(main):058:0> 3.times do irb(main):059:1* puts "hello" irb(main):060:1> end hello hello hello => 3According to our textbook, the usual convention is to use curly braces for short, one-line blocks, and to use do...end for multiline blocks.
Blocks can include parameters. This is very similar to an anonymous function in ML when we said things like:
fn x => 2 * xWe read this as, "a function of x that returns 2 * x." In Ruby you put any parameters inside pipe characters ("|") at the beginning of the block. After the parameter(s), you put the code, as in:
{|n| puts n}which we would read as, "a function of n that calls puts on n". We can pass this block to the times method:
irb(main):061:0> 3.times {|n| puts n} 0 1 2 => 3As you can see, the times method produces the values 0 through 2 as it executes the block three different times. Our earlier examples simply ignored this parameter value.
Then I said that I wanted to spend a little time understanding how Range objects are implemented in Ruby:
irb(main):066:0> x = 1..10 => 1..10 irb(main):068:0> x.class => RangeA common use for Range objects is to control the foreach loop in Ruby:
irb(main):069:0> for i in x irb(main):070:1> puts i irb(main):071:1> end 1 2 3 4 5 6 7 8 9 10 => 1..10We began by writing a constructor for it. In Ruby, you specify a constructor by overriding the initialize method:
class MyRange def initialize(first, last) @first = first @last = last end endIn Ruby, you differentiate between instance variables and local variables by putting an at-sign ("@") in front of any instance variable.
You construct objects by calling the new method of the class, although Ruby will make sure that you provide the right number of arguments:
irb(main):079:0> x = MyRange.new ArgumentError: wrong number of arguments (0 for 2) from (irb):79:in `initialize' from (irb):79:in `new' from (irb):79 from (null):0 irb(main):080:0> x = MyRange.new(1, 10) => #<MyRange:0xb7fa6c40 @first=1, @last=10>Then I asked people how to write a method that we'll call "eech" for now that simply prints every integer in the range from first to last. Someone suggested using a while loop:
def eech i = @first while i <= @last puts i i += 1 end endWe had to remember to put an @ in front of every instance variable name (a common error, especially for people used to Java). We forgot to include the increment of i in our first version, which gave us an infinite loop, but when we added it, we found that it printed the values, as expected:
irb(main):018:0> x.eech 1 2 3 4 5 6 7 8 9 10 => nilEveryone thought this was very boring until I said that we were about to see something really interesting. I said that instead of calling "puts" to print the value, what if we instead call "yield"?
def eech i = @first while i <= @last yield i i += 1 end endThe yield statement is used in Ruby to invoke a block. In fact, just including a call on yield caused Ruby to now insist on getting a block when the method is called:
irb(main):029:0> x.eech LocalJumpError: no block given from (irb):23:in `eech' from (irb):29 from :0Now we have to supply a block to execute, as in:
irb(main):031:0> x.eech {|n| puts 2 * n} 2 4 6 8 10 12 14 16 18 20 => nilHere's what is going on. The block represents some code that isn't immediately executed. It's passed to the eech method. The eech method does whatever it wants to, but then it calls yield as a way to invoke the block. At that point, control shifts to the block. The method called yield with a parameter, so that value is fed into the block into its parameter n. Once the block finishes executing, control goes back to the eech method. The eech method then does more work and calls yield again, shifting control back to the block. This back and forth continues until the eech method finishes executing.
I briefly discussed the idea of a block as a closure. When we studied ML, we saw that a closure has two key elements:
We found, though, that we couldn't use the built-in "for each" loop the way we can with a Range object. With the built-in Range, we can say:
for i in 1..5 puts i endBut we can't do the same with our MyRange object:
x = MyRange.new(1, 5) for i in x puts i endWhen we tried this, we got a NoMethodError for a method called "each". If you want your Ruby object to work with a for-each loop, you have to name the method "each". So we went back into the file and changed the name from "eech" to "each" and found that now we could use for-each loops for our MyRange object.
To explore this further, I showed a simple method that yields four times:
def f yield yield yield yield endThis method can be used to execute some bit of code four times, as in:
>> f {puts "hello"} hello hello hello hello => nilI 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" endWe can still pass simple code like before and it will execute four times:
>> f {puts "hello"} hello hello hello hello => nilBut with this version, we have the option of writing a block that includes a parameter:
>> f {|n| puts n * 3} 129 237 57 hellohellohello => nilWe can also have yield produce more than one result:
def f yield 43, 17 yield 79, 48 yield 19, "bar" yield "hello", 39 endWe 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 => nilIn 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 endWe 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" endIf 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 endwhich produces this output:
43 79 48 19 bar hello => nilNotice 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.
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 filterI 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 ... endAnd 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 endAnd what's left to do after that? We just have to return our result:
return resultSome 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:
resultI 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 endIt 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?"