if bool-expr [then] body elsif bool-expr [then] body else body end while bool-expr [do] body endRuby also allows you to include these after statements. So you can either say something like this:
irb(main):034:0> x = 3 irb(main):030:0> while x < 200 do irb(main):031:1* x *= 2 irb(main):032:1> end => nil irb(main):033:0> x => 384or you can say it this way:
irb(main):034:0> x = 3 => 3 irb(main):035:0> x *= 2 while x < 200 => nil irb(main):036:0> x => 384There are also interesting variations like an "unless" construct that is like an inverse if/else and an "until" construct that is like an inverse while.
Then I spent some time talking about classes. I started by pointing out that the Ruby philosophy is very different from the Java philosophy. In Java, a class definition contains the complete blueprint for the class, listing all instance variables and methods. In Ruby, you can define a class multiple times, each time adding more instance variables and methods. You can even do this for built-in classes.
I pointed out that the Array class has methods push and pop that give it stack-like behavior:
irb(main):001:0> x = [1, 2, 3, 4, 5] => [1, 2, 3, 4, 5] irb(main):002:0> x.push 17 => [1, 2, 3, 4, 5, 17] irb(main):003:0> x.push 98 => [1, 2, 3, 4, 5, 17, 98] irb(main):004:0> x.pop => 98 irb(main):005:0> x => [1, 2, 3, 4, 5, 17]We saw that we could use a class definition to dynamically add a new definition to the Array class:
irb(main):006:0> class Array irb(main):007:1> def push2(n) irb(main):008:2> push n irb(main):009:2> push n irb(main):010:2> end irb(main):011:1> end => nil irb(main):012:0> x.push2 3 => [1, 2, 3, 4, 5, 17, 3, 3] irb(main):013:0> x.push2 8 => [1, 2, 3, 4, 5, 17, 3, 3, 8, 8]We also saw that we could add new methods for numbers. For example, suppose we want to have a method called double that returns twice a number. There isn't such a method in ruby:
irb(main):060:0> 3.double NoMethodError: undefined method `double' for 3:Integer from (irb):60 from :0But that doesn't prevent us from adding it to the class:
irb(main):061:0> class Integer irb(main):062:1> def double irb(main):063:2> return 2 * self irb(main):064:2> end irb(main):065:1> end => nil irb(main):066:0> 3.double => 6I said that this is very powerful but also potentially dangerous. For example, you can redefine the addition operator:
irb(main):067:0> class Integer irb(main):068:1> def +(n) irb(main):069:2> return 5 irb(main):070:2> end irb(main):071:1> end => nil irb(main):072:0> 2 + 2 => 5 irb(main):005:0> 1 + 8 => 5 irb(main):005:0> 983 + 742 => 5 irb(main):005:0> 1 + 2 + 3 + 4 + 5 + 6 => 5It's interesting that you can do that, but that could potentially break other code that counts on addition behaving properly. For example, someone pointed out that the interpreter was no longer able to keep track of line numbers. It was reporting each line number as 5 after we made this change.
I then mentioned that I wanted to discuss one of the most important concepts in Ruby: the idea of a block. You can think of it as a "block of code," although it really is something we've seen before: a closure. You can specify blocks either with curly brace notation or with do...end notation. For example, the FixNum class has a method called times that expects a block. You get an error if you don't provide one:
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 => 3The 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 OCaml when we said things like:
fun 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..10The idea is to write our own version that we'll call MyRange. We 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 OCaml, 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.
The last thing we discussed was how to write getters and setters in Ruby. The getter part is easy:
def first() return @first endThe setter is more challenging because in Ruby instance variables aren't normally accessible to a client. There is a special syntax for writing a method that can change the value of a field using an assignment expression. You put an equals sign after the name of the field and use that as the name of the method, as in:
def first=(n) @first = n endOnce you have provided this method definition, you can use assignment expressions to change the field value as in:
irb(main):013:0> x = MyRange.new(10, 20) => #<MyRange:0x0000000146900168 @first=10, @last=20> irb(main):014:0> x.first = 0 => 0 irb(main):015:0> x => #<MyRange:0x0000000146900168 @first=0, @last=20>