if bool-expr [then]
body
elsif bool-expr [then]
body
else
body
end
while bool-expr [do]
body
end
Ruby 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
=> 384
or 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
=> 384
There 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 :0
But 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
=> 6
I 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
=> 5
It'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 :0
Using the curly brace notation we'd say:
irb(main):057:0> 3.times { puts "hello" }
hello
hello
hello
=> 3
The 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
=> 3
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 OCaml when we said things like:
fun x -> 2 * x
We 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
=> 3
As 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
=> Range
A 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..10
The 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
end
In 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
end
We 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
=> nil
Everyone 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
end
The 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 :0
Now 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
=> nil
Here'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
end
But we can't do the same with our MyRange object:
x = MyRange.new(1, 5)
for i in x
puts i
end
When 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
end
The 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
end
Once 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>
Remember that these are method calls, not references to instance variables.
Ruby provides some syntactic sugar for defining these getters and setters. You
can include an "attr_reader" clause to specify instance variables that you want
to have Ruby make a getter for. You include the name of the instance variable
as a symbol (with a colon in front), as in:
attr_reader :first, :last
There is a similar "attr_writer" clause that allows you to specify instance
variables that you want to have setters for:
attr_writer :first, :last
Then I pointed out that we can indicate default values for any method,
including the initialize method that is used to construct objects:
def intialize(first = 1, last = 10)
Then we wrote a "to string" method that Ruby calls to_s. I asked Ruby to show
me what the standard Range class does:
irb(main):004:0> (1..10).to_s
=> "1..10"
To recreate this behavior we can do it the Java way by converting each element
to a string and then concatenating them together:
def to_s
return @first.to_s + ".." + @last.to_s
end
Ruby offers an alternative where you can put together just a single string. In
a way, what you want to return is:
"@first..@last"
The problem is that we don't want the text "@first", we want to convert that
into a string. You can do that using a special form that starts with # and
then has an expression inside curly braces:
#{<expression>}
Ruby evaluates the expression and then calls to_s on the resulting object, as
in:
irb(main):005:0> "hello#{2 + 3 - 1}."
=> "hello4."
Ruby first evaluated the arithmetic to turn 2 + 3 - 1 into 4 and then it called
to_s on the 4 object.So we can rewrite our to_s as:
def to_s
return "#{@first}..#{@last}"
end
I showed one last detail. I asked how we could keep a count of how many
MyRange objects had been constructed. Obviously we want a counter that is
incremented inside the intialize method. But we can't make it an instance
variable because then every instance would have its own copy. We want a single
counter for the entire class. This is known as a class variable. Jave would
call it a static field. The Ruby convention is to use simple names like count
for local variables, names with an at-sign in front like @count for instance
variables, and names with a double at-sign in front like @@count for class
variables.We also had to have a way to get the value of count, so we introduced a new method to return its value:
def count
return @@count
end
attr_reader list. Putting all of these pieces together, we ended up with:
class MyRange
@@count = 0 # class variable to count # of objects constructed
def initialize(first = 0, last = 10)
@first = first
@last = last
@@count += 1
end
def count
return @@count
end
def each
i = @first
while i <= @last
yield i
i += 1
end
end
attr_reader :first, :last
attr_writer :first, :last
def to_s
return "#{@first}..#{@last}"
end
# without attr_reader and attr_writer, we would have done something like this
# def first
# return @first
# end
#
# def first=(n)
# @first = n
# end
end