Ruby

Inspired from ruby quickstart.

Ruby comes with a program that will show the results of any Ruby statements you feed it. Playing with Ruby code in interactive sessions like this is an excellent way to learn the language. If you’re using macOS open up Terminal and type irb (Interactive Ruby), then hit enter.

Type this:

irb(main):002:0> puts "Hello World"
Hello World
=> nil

puts is the basic command to print something out in Ruby. But then what’s the => nil bit? That’s the result of the expression. putsalways returns nil, which is Ruby’s absolutely-positively-nothing value.

Methods

What if we want to say Hello a lot without getting our fingers all tired? We need to define a method!

def hi
  puts "Hello World!"
end

The code def hi starts the definition of the method. It tells Ruby that we’re defining a method, that its name is hi. The next line is the body of the method, the same line we saw earlier: puts "Hello World". Finally, the last line end tells Ruby we’re done defining the method.

irb(main):013:0> hi
Hello World!
=> nil
irb(main):014:0> hi()
Hello World!
=> nil

Well, that was easy. Calling a method in Ruby is as easy as just mentioning its name to Ruby. If the method doesn’t take parameters that’s all you need. You can add empty parentheses if you’d like, but they’re not needed.

What if we want to say hello to one person, and not the whole world? Just redefine hi to take a name as a parameter.

def hi(name)
  puts "Hello #{name}!"
end
irb(main):018:0> hi("Matz")
Hello Matz!

What’s the #{name} bit? That’s Ruby’s way of inserting something into a string. The bit between the braces is turned into a string (if it isn’t one already) and then substituted into the outer string at that point.

Object oriented programming

If you haven't seen object oriented programming before, we recommend you to check this before continuing. If you still want to know more about object oriented programming in Ruby, we recommend checking this course.

Classes

What if we want a real greeter around, one that remembers your name and welcomes you and treats you always with respect. You might want to use an object for that. Let’s create a Greeter class.

class Greeter
  def initialize(name = "World")
    @name = name
  end

  def say_hi
    puts "Hi #{@name}!"
  end

  def say_bye
    puts "Bye #{@name}, come back soon."
  end
end

The new keyword here is class. This defines a new class called Greeter and a bunch of methods for that class. Also notice @name. This is an instance variable, and is available to all the methods of the class. As you can see it's used by say_hi and say_bye.

Now let’s create a greeter object and use it:

irb(main):035:0> greeter = Greeter.new("Pat")
=> #<Greeter:0x16cac @name="Pat">
irb(main):036:0> greeter.say_hi
Hi Pat!
=> nil
irb(main):037:0> greeter.say_bye
Bye Pat, come back soon.
=> nil

Once the greeter object is created, it remembers that the name is Pat. Hmm, what if we want to get at the name directly?

irb(main):038:0> greeter.@name
SyntaxError: (irb):38: syntax error, unexpected tIVAR, expecting '('

Nope, can’t do it.

Instance variables are hidden away inside the object. They’re not terribly hidden, you see them whenever you inspect the object, and there are other ways of accessing them, but Ruby uses the good object-oriented approach of keeping data sort-of hidden away.

So what methods do exist for Greeter objects?

irb(main):039:0> Greeter.instance_methods
=> [:say_hi, :say_bye, :instance_of?, :public_send,
    :instance_variable_get, :instance_variable_set,
    :instance_variable_defined?, :remove_instance_variable,
    :private_methods, :kind_of?, :instance_variables, :tap,
    :is_a?, :extend, :define_singleton_method, :to_enum,
    :enum_for, :<=>, :===, :=~, :!~, :eql?, :respond_to?,
    :freeze, :inspect, :display, :send, :object_id, :to_s,
    :method, :public_method, :singleton_method, :nil?, :hash,
    :class, :singleton_class, :clone, :dup, :itself, :taint,
    :tainted?, :untaint, :untrust, :trust, :untrusted?, :methods,
    :protected_methods, :frozen?, :public_methods, :singleton_methods,
    :!, :==, :!=, :__send__, :equal?, :instance_eval, :instance_exec, :__id__]

Whoa. That’s a lot of methods. We only defined two methods. What’s going on here? Well this is all of the methods for Greeter objects, a complete list, including ones defined by ancestor classes. If we want to just list methods defined for Greeter we can tell it to not include ancestors by passing it the parameter false, meaning we don’t want methods defined by ancestors.

irb(main):040:0> Greeter.instance_methods(false)
=> [:say_hi, :say_bye]

But what if you want to be able to view or change the name? Ruby provides an easy way of providing access to an object’s variables.

irb(main):044:0> class Greeter
irb(main):045:1>   attr_accessor :name
irb(main):046:1> end
=> nil

In Ruby, you can open a class up again and modify it. The changes will be present in any new objects you create and even available in existing objects of that class. So, let’s create a new object and play with its @name property.

irb(main):047:0> greeter = Greeter.new("Andy")
=> #<Greeter:0x3c9b0 @name="Andy">
irb(main):048:0> greeter.respond_to?("name")
=> true
irb(main):049:0> greeter.respond_to?("name=")
=> true
irb(main):050:0> greeter.say_hi
Hi Andy!
=> nil
irb(main):051:0> greeter.name="Betty"
=> "Betty"
irb(main):052:0> greeter
=> #<Greeter:0x3c9b0 @name="Betty">
irb(main):053:0> greeter.name
=> "Betty"
irb(main):054:0> greeter.say_hi
Hi Betty!
=> nil

Using attr_accessor defined two new methods for us, name to get the value, and name= to set it.

Flow control

This greeter isn't all that interesting though, it can only deal with one person at a time. What if we had some kind of MegaGreeter that could either greet the world, one person, or a whole list of people?

Let’s write this one in a file instead of directly in the interactive Ruby interpreter IRB.

#!/usr/bin/env ruby

class MegaGreeter
  attr_accessor :names

  # Create the object
  def initialize(names = "World")
    @names = names
  end

  # Say hi to everybody
  def say_hi
    if @names.nil?
      puts "..."
    elsif @names.respond_to?("each")
      # @names is a list of some kind, iterate!
      @names.each do |name|
        puts "Hello #{name}!"
      end
    else
      puts "Hello #{@names}!"
    end
  end

  # Say bye to everybody
  def say_bye
    if @names.nil?
      puts "..."
    elsif @names.respond_to?("join")
      # Join the list elements with commas
      puts "Goodbye #{@names.join(", ")}.  Come back soon!"
    else
      puts "Goodbye #{@names}.  Come back soon!"
    end
  end
end



mg = MegaGreeter.new
mg.say_hi
mg.say_bye

# Change name to be "Zeke"
mg.names = "Zeke"
mg.say_hi
mg.say_bye

# Change the name to an array of names
mg.names = ["Albert", "Brenda", "Charles",
            "Dave", "Engelbert"]
mg.say_hi
mg.say_bye

# Change to nil
mg.names = nil
mg.say_hi
mg.say_bye

Save this file as mega_greeter.rb, and run it as ruby mega_greeter.rb. The output should be:

Hello World!
Goodbye World.  Come back soon!
Hello Zeke!
Goodbye Zeke.  Come back soon!
Hello Albert!
Hello Brenda!
Hello Charles!
Hello Dave!
Hello Engelbert!
Goodbye Albert, Brenda, Charles, Dave, Engelbert.  Come
back soon!
...
...

So, looking deeper at our new program, notice the initial lines, which begin with a hash mark (#). In Ruby, anything on a line after a hash mark is a comment and is ignored by the interpreter. The first line of the file is a special case, and under a Unix-like operating system (Linux, macOS, etc.) tells the shell how to run the file. The rest of the comments are there just for clarity.

Our say_hi method has become a bit trickier:

# Say hi to everybody
def say_hi
  if @names.nil?
    puts "..."
  elsif @names.respond_to?("each")
    # @names is a list of some kind, iterate!
    @names.each do |name|
      puts "Hello #{name}!"
    end
  else
    puts "Hello #{@names}!"
  end
end

It now looks at the @names instance variable to make decisions. If it's nil, it just prints out three dots. No point greeting nobody, right?

Cycling and Looping

If the @names object responds to each, it is something that you can iterate over, so iterate over it and greet each person in turn. Finally, if @names is anything else, just let it get turned into a string automatically and do the default greeting.

Let's look at that iterator in more depth:

@names.each do |name|
  puts "Hello #{name}!"
end

each is a method that accepts a block of code then runs that block of code for every element in a list, and the bit between do and end is just such a block. A block is like an anonymous function. The variable between pipe characters (|) is the parameter for this block.

What happens here is that for every entry in a list, name is bound to that list element, and then the expression puts "Hello #{name}!" is run with that name.

Most other programming languages handle going over a list using the for loop, which in C looks something like:

for (i = 0; i < number_of_elements; i++)
{
  do_something_with(element[i]);
}

This works, but isn’t very elegant. You need a throw-away variable like i, have to figure out how long the list is, and have to explain how to walk over the list. The Ruby way is much more elegant, all the housekeeping details are hidden within the each method, all you need to do is to tell it what to do with each element. Internally, the each method will essentially call yield "Albert", then yield "Brenda" and then yield "Charles", and so on.

Ruby Style Guide

Writing high quality code is essential to have easy to read and easy to maintain products. Please check the ruby styleguide.

Additional resources