~/src/www.mokhan.ca/xlgmokha [main]
cat ruby-programming-guide.md
ruby-programming-guide.md 35272 bytes | 2011-06-01 12:00
symlink: /opt/ruby/ruby-programming-guide.md

Ruby Programming Guide

This is a collection of notes covering Ruby programming fundamentals and advanced concepts.

Installation and Setup

Installing Ruby with RVM

$ sudo apt-get update
$ sudo apt-get install curl
$ bash < <( curl -s https://rvm.beginrescueend.com/install/rvm )

Add to .bashrc:

source $HOME/.rvm/scripts/rvm

Then install dependencies and Ruby:

$ rvm notes # follow instructions to install missing libs
$ sudo apt-get install build-essential bison openssl libreadline5 libreadline-dev curl git-core zliblg zliblg-dev libssl-dev vim libsqlite3-0 libsqlite3-dev sqlite3 libreadline-dev libxml2-dev git-core subversion autoconf
$ rvm install 1.9.2
$ rvm use 1.9.2
$ ruby -v

Documentation

$ ri GC
$ ri GC::enable
$ ri assoc

Core Data Types

Arrays and Hashes

Arrays and hashes are indexed collections. Both store collections of objects, accessible using a key.

Array literal:

a = [ 1, 'cat', 3.14 ] # array with three elements
puts "the first element is #{a[0]}"

Ruby has a shortcut for creating arrays of strings:

a = [ 'ant', 'bee', 'cat', 'dog', 'elk' ]
a[0] # => "ant"
a = %w[ ant bee cat dog elk ]
a[0] # => "ant"

Hashes are similar to arrays. You must supply two objects for every entry, one for the key, the other for the value:

dead_rappers = {
  'tupac'   => '1996',
  'biggie'  => '1997',
  'big l'   => '1999',
  'eazy e'  => '1995',
  'odb'     => '2004'
}

p dead_rappers['tupac']   # => '1996'
p dead_rappers['biggie']  # => '1997'
p dead_rappers['big pun'] # => nil

A hash by default returns nil when indexed by a key it doesn’t contain. To change the default value you can specify it when creating a new hash:

histogram = Hash.new(0) # the default value 0
histogram['ruby'] # => 0
histogram['ruby'] = histogram['ruby'] + 1
histogram['ruby'] # => 1

Symbols

Symbols are simple constant names that you don’t have to predeclare and that are guaranteed to be unique. A symbol literal starts with a colon and is normally followed by some kind of name:

walk(:north)
walk(:east)

There’s no need to assign some kind of value to a symbol - Ruby takes care of that for you. Ruby also guarantees that no matter where it appears in your program, a particular symbol will have the same value.

def walk(direction)
  if direction == :north
    # ...
  end
end

Symbols are frequently used as keys in hashes:

dead_rappers = {
  :tupac  => '1996',
  :biggie => '1997',
  :bigl   => '1999',
  :eazye  => '1995',
  :odb    => '2004'
}

dead_rappers[:odb] # => '2004'

In Ruby 1.9+ you can use name: value pairs to create a hash if the keys are symbols:

dead_rappers = {
  tupac:  '1996',
  biggie: '1997',
  bigl:   '1999',
  eazye:  '1995',
  odb:    '2004'
}
puts "big l died in #{dead_rappers[:bigl]}"

Control Structures

Conditionals

The if/elsif/else structure:

today = Time.now

if today.saturday?
  puts "do chores around the house"
elsif today.sunday?
  puts "chill out"
else
  puts "go to work"
end

Loops

The while loop:

i = 0
while i < 100
  i += 1
end

Ruby treats nil as false, so you can write:

while line = gets
  puts line.downcase
end

Statement Modifiers

Statement modifiers are a cool shortcut if the body of an if or while statement is just a single expression:

if radiation > 3000
  puts "danger, will robinson"
end

puts "danger, will robinson" if radiation > 3000

square = 4
while square < 1000
  square = square * square
end

square = square * square while square < 1000

Blocks and Iterators

Basic Blocks

This is a code block:

{ puts "hello" }

So is this:

do 
  club.enroll(person)
  person.socialize
end

The only thing you can do with a block is associate it with a call to a method. You can do this by putting the start of the block at the end of the source line containing the method call:

greet { puts "hi" }

The method greet is given a block. If the method has parameters, then they appear before the block:

verbose_greet("mo", "a geek") { puts "hi" }

Using yield

A method can invoke the block by calling yield:

def call_block
  puts "start of method"
  yield
  yield
  puts "end of method"
end

call_block { puts "in the block" }

Output:

start of method
in the block
in the block
end of method

You can also pass arguments to the block. Within the block you need to list the names of the parameters to receive these arguments between vertical bars:

def who_says_what
  yield("mo", "hello")
  yield("morilla", "yo")
end

who_says_what {|person, phrase| puts "#{person} says #{phrase}" }

Common Iterators

Code blocks are used throughout the Ruby library to implement iterators:

animals = %w( ant bee cat dog elk )
animals.each {|animal| puts animal }
[ 'cat', 'dog', 'horse' ].each {|name| print name, " "}
5.times { print "*" }
3.upto(6) {|i| print i }
('a'..'e').each {|char| print char}

Classes and Objects

Basic Class Definition

class Book
  def initialize(isbn, price)
    @isbn = isbn
    @price = price
  end
end

book = Book.new("isbn", 3)

initialize is a special method in Ruby programs. When you call Book.new to create a new object, Ruby allocates memory to hold an uninitialized object and then calls that object’s initialize method.

@isbn is an instance field of the class. When you want to declare an instance field you must prefix the name of the variable with the @ sign.

Custom String Representation

To customize how an object is displayed via puts or p you can override the to_s method:

class Movie
  def initialize(name, studio, year_published)
    @name = name
    @studio = studio
    @year_published = year_published
  end

  def to_s
    "Name: #{@name}, Studio: #{@studio}, Year: #{@year_published}"
  end
end

p Movie.new("MegaMind", "DreamWorks", 2010)

Attributes (Properties)

By default instance variables are private and cannot be accessed from outside the class. You can define “attributes” on the class to be able to see and change the internal state of the class.

You can define read-only attributes explicitly:

class VideoGame
  def initialize(name)
    @name = name
  end
  
  def name
    @name
  end
end

game = VideoGame.new("call of duty")
puts game.name

Ruby has a handy shorthand called attr_reader:

class VideoGame
  attr_reader :name

  def initialize(name)
    @name = name
  end
end

game = VideoGame.new("call of duty")
puts game.name

To create a setter method, define a method named with =:

class VideoGame
  def initialize(name)
    @name = name
  end

  def name=(new_name)
    @name = new_name
  end
end

game = VideoGame.new("call of duty")
game.name = "GTA4"

There’s also a shorthand for setter methods called attr_writer:

class VideoGame
  attr_writer :name

  def initialize(name)
    @name = name
  end
end

game = VideoGame.new("call of duty")
game.name = "GTA4"

For both getter and setter, use attr_accessor:

class VideoGame
  attr_accessor :name

  def initialize(name)
    @name = name
  end
end

game = VideoGame.new("call of duty")
game.name = "GTA4"
puts game.name

Access Control

You get 3 levels of protection:

  • Public methods (default): can be called by anyone
  • Protected methods: can be invoked by defining class and subclasses
  • Private methods: can only be called within the context of the current object

It is never possible to access another object’s private methods directly, even if the object is of the same class as the caller.

class MyClass
  def public_method
  end

protected
  def protected_method
  end

private
  def private_method
  end

public 
  def another_public_method
  end
end

# Alternative syntax:

class AnotherClass
  def public_method
  end

  def protected_method
  end

  def private_method
  end

  public      :public_method
  protected   :protected_method
  private     :private_method
end

Variables and Object References

Variables are used to keep track of objects; each variable holds a reference to an object. A variable is simply a reference to an object:

movie = Movie.new("man on fire")
another_movie = movie
movie = Movie.new("batman")

puts movie.name        # => "batman"
puts another_movie.name # => "man on fire"

Testing and Development Practices

Custom RSpec Matchers

module RSpec
  Matchers.define :contain do |*expected_items|
    match do |results|
      expected_items.each do |item|
        results.include?(item).should be_true
      end
    end
  end

  Matchers.define :contain_in_order do |*expected_items|
    match do |results|
      expected_items.should == results
    end
  end
end

Load Path Management

Understanding $LOAD_PATH and $:. $: is shorthand for $LOAD_PATH. unshift adds an item to the start of an array:

Dir.glob('lib/**/*.rb').each{|item| $:.unshift(File.dirname(item))}

Dir.glob('lib/*.rb').each do |f|
  require File.basename(f, '.rb')
end

Building DSLs

Example of building an internal DSL for queries:

it 'should be able to find all movies published by pixar' do
  results = sut.all_items_matching Where.item(:studio).is_equal_to(ProductionStudio.Pixar)
  results.should == [ cars, a_bugs_life ]
end