CSE341 Notes for Monday, 11/30/09

We continued our exploration of Ruby. We were able to ask each object what class it is by saying things like 3.class or "hello".class. This is called reflection and is an important part of modern programming languages. The ability to ask for 3.methods is another example of reflection. Java has a great deal of support for reflection through packages like java.lang.reflect, although it's much easier to do this in Ruby. We saw that we could ask any object for its methods, as in 3.methods (methods of integers) or "hello".methods (methods of strings) or 3.methods.methods (methods of arrays).

We saw that we could define new methods with def...end, as in:

        def f(n)
          return n * 2
        end
This defines a function f that doubles a number (f(8) returns 16). This looked like a static method in Java, but in fact it is defined in a default class called "main". We were able to see it by asking for self.methods ("self" is the Ruby equivalent of Java's "this" keyword).

I pointed out that we can actually pass all sorts of values to function f:

        irb(main):004:0> f(12)
        => 24
        irb(main):005:0> f(3.8)
        => 7.6
        irb(main):006:0> f("hello")
        => "hellohello"
        irb(main):007:0> f([1, 2, 3])
        => [1, 2, 3, 1, 2, 3]
This is an example of the "duck" typing that Ruby uses. What matters to Ruby is that the value you pass to the function is able to respond to the message "* 2". If it can do that (if it can quack that way), then it's okay (it's the kind of duck this function is expecting).

We saw that Ruby has a much more consistent syntax for structured objects compared to Java:

        structure            # elems?        elem i?
        ------------------------------------------------
        Java array           a.length        a[i]
        Java String          a.length()      a.charAt(i)
        Java ArrayList       a.size()        a.get(i)

        Ruby (all)           x.length        x[i]
We also saw that Ruby has two ways to define a "slice":

        x[m, n]  #n values starting at index m
        x[m..n]  #values between indexes m and n inclusive
I mentioned that there is a class called Hash that can be used to store a hashtable of key/value pairs (like the Java Map classes). You can construct it by calling new or just using curly brace notation:

        irb(main):020:0> x = Hash.new
        => {}
        irb(main):021:0> x = {}
        => {}
Once constructed, you can use array-like square bracket notation to associate keys with values:

        irb(main):022:0> x["hello"] = 15
        => 15
        irb(main):023:0> x["foo"] = 92
        => 92
        irb(main):024:0> x[85] = "bar"
        => "bar"
        irb(main):025:0> x
        => {85=>"bar", "foo"=>92, "hello"=>15}
This is simpler than the Java approach of calling get and put methods, especially when you want to do complex manipulations like:

        irb(main):026:0> x["foo"] *= 2
        => 184
        irb(main):027:0> x
        => {85=>"bar", "foo"=>184, "hello"=>15}
I then talked a bit about control structures. I said that I really like the "quick reference guide" that is linked under Ruby resources on the class web page. Ruby has many familiar control structures like if/else and while and the quick reference has templates for these:

        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.

For example, we've seen the built-in Array class:

        irb(main):037:0> x = [1, 2, 3, 4, 5]
        => [1, 2, 3, 4, 5]
        irb(main):038:0> x.class
        => Array
We saw that we could use a class definition to dynamically add a new definition to the Array class:

        irb(main):046:0> class Array
        irb(main):047:1>   def push2(n)
        irb(main):048:2>     push n
        irb(main):049:2>     push n
        irb(main):050:2>     end
        irb(main):051:1>   end
        => nil
        irb(main):052:0> x.push2 3
        => [8, 3, 3]
        irb(main):053:0> x.push2 8
        => [8, 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:Fixnum
                from (irb):60
                from :0
But that doesn't prevent us from adding it to the class:

        irb(main):061:0> class Fixnum
        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 Fixnum
        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
According to our textbook, the usual convention is to use curly braces for short, one-line blocks, and to use do...end for multiline blocks.


Stuart Reges
Last modified: Tue Dec 1 21:17:36 PST 2009