We saw that we could define new methods with def...end, as in:
def f(n) return n * 2 endThis 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 inclusiveI 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 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.
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 => ArrayWe 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 :0But 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 => 6I 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 => 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 => 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.