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. puts
always 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
- The odin project ruby course