**Elements of Ruby**
Stijn Heymans
published: 17 December 2021
# Introduction
I will introduce you to the elements of programming in
[Ruby](https://www.ruby-lang.org). I will assume you can program in some other
language such as Java,
[Perl](http://www.stijnheymans.net/elements_of_perl.html), Python, Go, ...
Background sources that were helpful to me:
- [Why's (Poignant) Guide to Ruby](https://poignant.guide/) by _why the lucky
stiff_. More art project than programming book, but a beginner's resource
that gives an elemental overview of the language _with_ intuition of the Ruby
language. If you have read that book, you can stop reading this article.
There's nothing here for you.
- [Eloquent Ruby](https://www.goodreads.com/book/show/9364729-eloquent-ruby) by
Russ Olsen. A bit more advanced than _Why's (Poignant) Guide to Ruby_. The
book ventures into meta-programming with Ruby. To see what sets Ruby apart, I
recommend this book.
In this article, I will follow a structure that is loosely inspired by the
structure of [Introducing
Go](https://www.goodreads.com/book/show/27015358-introducing-go) by Caleb
Doxsey. Not a Ruby book, but a Go book. It's a _short_ book on Go that manages
to bring the basics across. Something I aspire to in this article for Ruby.
# Getting Started
On the command-line, check whether Ruby is installed:
~~~shell
$ ruby --version
~~~
For me, this shows `ruby 2.6.3p62 (2019-04-16 revision 67580)
[universal.x86_64-darwin20]`). If you don't have Ruby installed, get it at [the
ruby-lang website](https://www.ruby-lang.org/en/downloads/).
If you have Ruby installed, you also have an interactive interpreter called
[irb](https://github.com/ruby/irb). On the command-line, type `irb`.
~~~none
irb> puts 'Hello World!'
Hello World!
=> nil
~~~
Leave the interactive interpreter `irb` by typing `exit`. Now say `Hello
World!` again, but without `irb`. Open a file (make it end on `rb`, for example
`ruby_playground.rb`) and write the following line in it:
~~~ruby
puts 'Hello World!'
~~~
Close the file and at the command-line have Ruby interpret it:
~~~none
$ ruby ruby_playground.rb
Hello World!
~~~
Do you want to know more about `puts`? Get in the habit of using the [official
docs](https://docs.ruby-lang.org/en/). Pick the right version of the docs
corresponding to what you saw in your initial `ruby --version`. I picked
`2.6.0` as my Ruby version is `2.6.3` and found this
[explanation](https://docs.ruby-lang.org/en/2.6.0/ARGF.html#method-i-puts).
# The Basic Classes
## Numbers
Unsurprisingly, integers are represented as `3`, `5`, `-100`, ...
Surprisingly, everything in Ruby is an object so you can ask the class of an
integer: `5.class`. This returns
[Integer](https://docs.ruby-lang.org/en/2.6.0/Integer.html).
Since integers are objects there's more fun stuff one can ask them:
~~~none
irb> 123.digits
=> [3, 2, 1]
~~~
Fans of coding interviews will appreciate this.
Simple math still works despite rumors:
~~~none
irb> 1 + 2
=> 3
irb> 2 * 3
=> 6
irb> 8 / 4
=> 2
~~~
Real but inexact numbers are of type
[Float](https://docs.ruby-lang.org/en/2.6.0/Float.html). In its literal
representation, it's _things_ that contain a decimal point:`1.0`, `9.9999`, ...
~~~none
irb> 1.0.class
=> Float
~~~
First encounters are frequently unpleasant:
~~~none
irb> 7 / 2
=> 3
~~~
Dividing 2 integers results in integer division. Force one to be a `Float`
using `to_f`:
~~~none
irb> 7.to_f / 2
=> 3.5
~~~
## Strings
String are sequences of characters surrounded by single or double
quotes: `'Hello'` or `"Hello"`. The double-quoted variety is special. More
later. Be patient.
Use `+` to concatenate 2 Strings without changing the original Strings. Use
`<<` to concatenate and destroy the String you're appending to:
~~~none
irb> a = "hello"
=> "hello"
irb> b = "world"
=> "world"
irb> a + b
=> "helloworld"
irb> a
=> "hello"
irb> b
=> "world"
irb> a << b
=> "helloworld"
irb> a
=> "helloworld"
irb> b
=> "world"
~~~
Pull characters out of a String:
~~~none
irb> a = "hello"
=> "hello"
irb> a[0]
=> "h"
irb> a[-1]
=> "o"
~~~
Get substrings by using inclusive (`..`) or exclusive (`...`) ranges:
~~~none
irb> a = "hello"
=> "hello"
irb> a[0..2]
=> "hel"
irb> a[0...2]
=> "he"
~~~
## Symbols
Symbols consist of letters, digits, or underscores, and are preceded by a `:` .
For example, `:a`, `:a_2`, `:an_orange`. Why not just use Strings? [Great
question](https://stackoverflow.com/questions/255078/whats-the-difference-between-a-string-and-a-symbol-in-ruby).
Explore how Strings are different from symbols by asking for their
`object_id`:
~~~none
irb> "bar".object_id
=> 70236502854760
irb> "bar".object_id
=> 70236502887440
irb> :foo.object_id
=> 1516508
irb> :foo.object_id
=> 1516508
~~~
From that `irb` session, note that
- every object has a method `object_id` that returns _some_ id.
- Strings that appear the same do not necessarily have the same `object_id`, which _probably_ has memory consequences
- symbols that appear the same have the same `object_id`, which _probably_ has memory consequences
## Booleans
`true` and `false` $\square$
# Variables
Like symbols, variables consist of letters, digits, or underscores. Unlike symbols,
they do not start with colons. You initialize them:
~~~none
irb> x = 'hello'
=> "hello"
irb> x = 5
=> 5
irb> x = true
=> true
~~~
I assigned and re-assigned that `x` to my heart's content with different values
_and_ different types.
You feel resistance. "But this is insane," you say. You do not like it and you
want the world to know it. You take the matter to twitter, but twitter does not
care.
Single and double quotes are different:
~~~none
irb> x = 2
=> 2
irb> "we were #{x}"
=> "we were 2"
irb> 'we were #{x}'
=> "we were \#{x}"
~~~
[String interpolation](https://en.wikipedia.org/wiki/String_interpolation) yes.
And sometimes, variables are not variable:
~~~none
irb> X = 5
=> 5
irb> X = 6
(irb):5: warning: already initialized constant X
~~~
Capitalize a variable and it becomes _constant_.
A word on camels and snakes when naming in Ruby. Use snake case almost
everywhere: `lowercase_words_separated_by_underscores`. Except `ClassNames` and
`ModuleNames`, go for camel case there. Later more on classes and modules.
# Control Structures
## Conditions and `if`
The basic `if`:
~~~ruby
if condition
do_something
end
~~~
No parentheses or curly brackets for the condition. No colon after the
condition. `end` to end.
`else if` and `else`:
~~~ruby
if condition
do_something
elsif other_condition
do_something_else
else
final_thing_you_can_do
end
~~~
Conditions in `if` statements are expressions that evaluate to true or false.
What expressions evaluates to true?
- all numbers (including `0`) are true
- all Strings (including `''` -- the empty String) are true
- `nil` (yep, that's a null) is false
- the boolean literal `true` is true
- the boolean literal `false` is false
- the boolean expression `a && b` is true if `a` and `b` are true
- the boolean expression `a || b` is true if `a` or `b` are true
- the boolean expression `!a` is true if a is not true
Conditions often compare things: if variable `a` is equal to `"dog"`,
then do this. Ruby has `==`, `===`, `equal?`, and `eql?`. You
will mostly use:
- `==` for checking whether values are equal as far as the class designer of the objects you're comparing is concerned.
- `equal?` for checking object equality.
In fact, `equal?` and `==` as defined in the
[`Object`](https://ruby-doc.org/core-3.0.2/Object.html#method-i-eql-3F) class
are equal. _But how are they equal? How?_
Class designers can override `==` to something that makes sense for the class.
`equal?` should stand for object equality. Take the `String` class:
~~~none
irb> "a" == "a"
=> true
irb> "a".equal?("a")
=> false
~~~
So `"a"` is equal to `"a"` as values, but, as we saw before, they're not the same
objects. However, for symbols:
~~~none
irb> :a == :a
=> true
irb> :a.equal?(:a)
=> true
~~~
Back to our `if`. Instead of:
~~~ruby
if !condition
do_something
end
~~~
write:
~~~ruby
unless condition
do_something
end
~~~
Furthermore, if `do_something` is just one line, Ruby devs often prefer a
one-liner:
~~~ruby
do_something if condition
~~~
or
~~~ruby
do_something unless condition
~~~
Especially when returning early from functions, this makes code readable:
~~~ruby
return if invalid_parameters
~~~
## Looping
Take a look at a relatively standard loop-pattern in programming. For example, this `go` loop:
~~~go
n := 5
for i := 0; i < n; i++ {
fmt.Println(i)
}
~~~
This prints:
~~~none
0
1
2
3
4
~~~
There's a variety of ways to write this in Ruby, but the above `for`
construct is not one of them. Closest in spirit to the original is to
use a `while`:
~~~ruby
n = 5
i = 0
while i < n
puts i
i += 1
end
~~~
This is more verbose than the corresponding `for` loop in `go`.
More idiomatic Ruby uses [`times`](https://docs.ruby-lang.org/en/2.6.0/Integer.html#method-i-times):
~~~ruby
n.times { |i| puts i }
~~~
More elegant than the clunky `while`. Deconstruct this expression:
- `n`. An `Integer`, and thus an object that has methods.
- `times`. A
[method](https://docs.ruby-lang.org/en/2.6.0/Integer.html#method-i-times)
that is called on the object `n` and _iterates_ the following block `{ |i|
puts i}` from `0` to `n - 1`, passing in those values to `i` with each
iteration. More on blocks later, but for those familiar, the block is
essentially an anonymous function or lambda with parameter `i` (or arrow
function for the Javascript knowledgeables).
- `{ |i| puts i }`. We spoiled it. This is a `block` that prints `i` with `i`
ranging over values `0` to `n - 1`. This _ranging over values_ is
accomplished by `times`. We'll see later how we can write functions such as
`times` ourselves in the section on Blocks. For now, you can see `times` as
an higher-order function that takes another function as an argument.
Most of the time your looping will be in the context of some collection so I'll
talk more about looping then.
# Collections
## Arrays
An array is an [ordered integer-indexed
collection](https://docs.ruby-lang.org/en/2.6.0/Array.html) of objects.
Initialize an array:
~~~none
irb> a = ["x", 1, :y]
=> ["x", 1, :y]
~~~
Arrays are not restricted to contain only 1 type of object. You can shove in
there what you fancy:
~~~none
irb> a = ["x", [1, 2], :y]
=> ["x", [1, 2], :y]
~~~
The usual suspects with a special mention for `-1` (the last of the elements):
~~~none
irb> a = ["x", [1, 2], :y]
=> ["x", [1, 2], :y]
irb> a.length
=> 3
irb> a[1]
=> [1, 2]
irb> a[-1]
=> :y
~~~
Add elements to the back of an array using `<<` or `push`. Both are
destructive operations:
~~~none
irb> a << :b
=> ["x", [1, 2], :y, :b]
irb> a.push(:c)
=> ["x", [1, 2], :y, :b, :c]
irb> a
=> ["x", [1, 2], :y, :b, :c]
~~~
Remove that last element with `pop`:
~~~none
irb> l = a.pop
=> :c
irb> a
=> ["x", [1, 2], :y, :b]
~~~
What about adding and removing to the _front_ of an array? Use `unshift` and
`shift`. As far as unsavory mnemonics go, remove the `f` to remember which does
what.
~~~none
irb> a
=> ["x", [1, 2], :y, :b]
irb> f = a.shift
=> "x"
irb> a
=> [[1, 2], :y, :b]
irb> a.unshift("x_new")
=> ["x_new", [1, 2], :y, :b]
~~~
`pop/push` must be more efficient than `shift/unshift`? Seems it [hardly
matters](https://stackoverflow.com/questions/8353026/what-is-the-run-time-of-shift-unshift-in-a-ruby-array).
What if you have a second array `b = [:b1, :b2]` that you want to append (i.e.,
add each element) to array `a`? `push` does not do what you want:
~~~none
irb> a = ["x", [1, 2], :y]
=> ["x", [1, 2], :y]
irb> b = [:b1, :b2]
=> [:b1, :b2]
irb> a.push(b)
=> ["x", [1, 2], :y, [:b1, :b2]]
~~~
However, using the _splat_ operator `*` will:
~~~none
irb> b = [:b1, :b2]
=> [:b1, :b2]
irb> a.push(*b)
=> ["x", [1, 2], :y, :b1, :b2]
~~~
`*b` for an array `b` in the context of a function call (`push` here) sends
each element of the array `b` as an individual argument to the function. Thus,
`a.push(*b)` is the same as `a.push(:b1, :b2)`.
I initialized arrays using their literal form (such as `a = [1, 2]`). I can
also initialize them using the `new` method from the
[Array](https://docs.ruby-lang.org/en/2.6.0/Array.html) class:
~~~none
irb> a = Array.new
=> []
irb> a << 1
=> [1]
~~~
## Hashes
[Hashes](https://docs.ruby-lang.org/en/2.6.0/Hash.html) store key-value pairs
with unique keys (also called dictionaries, associative arrays, or
maps in other languages). Any object can be a key.
Define a hash that stores a first and last name, get a
value for an existing/non-existing key, change a value for an existing
key, add a value for a non-existing key, and remove a key/value pair:
~~~none
irb> a = { "first" => "John", "last" => "Doe" }
=> {"first"=>"John", "last"=>"Doe"}
irb> a["first"]
=> "John"
irb> a["nothere"]
=> nil
irb> a["first"] = "Jane"
=> "Jane"
irb> a
=> {"first"=>"Jane", "last"=>"Doe"}
irb> a["first"]
=> "Jane"
irb> a["nothere"] = "now it is here"
=> "now it is here"
irb> a
=> {"first"=>"Jane", "last"=>"Doe", "nothere"=>"now it is here"}
irb> a.delete("nothere")
=> "now it is here"
irb> a
=> {"first"=>"Jane", "last"=>"Doe"}
~~~
Any object can be a value in a Hash. Any object can be a key in a Hash. In the
above example, all keys were Strings. Consider this example with symbols as
keys:
~~~none
irb> a = { :first => "John", :last => "Doe" }
=> {:first=>"John", :last=>"Doe"}
irb> a[:first]
=> "John"
~~~
The example used symbols `:first` and `:last` as keys. If you use symbols as
keys, you can party as wild as JSON on a Friday night:
~~~none
irb> a = { first: "John", last: "Doe" }
=> {:first=>"John", :last=>"Doe"}
~~~
I said you can use any object as a hash key. This relies on properly overriding
`hash` and `eql?` (the latter is what
[Hash](https://docs.ruby-lang.org/en/2.6.0/Hash.html) uses for testing equality
of keys. [Two objects will then refer to the same key when their `hash` value
is identical and the 2 objects `eql?` each
other](https://docs.ruby-lang.org/en/2.6.0/Hash.html#class-Hash-label-Hash+Keys).
Elemental? Probably not. Moving on.
## Looping through Arrays and Hashes
You will spend half of your life looping through arrays and hashes. Given an array:
~~~none
a = [100, 200, 300]
~~~
Print each element:
~~~none
a.each { |el| puts el }
~~~
No `for` in sight. Use `each` when you can. `each` iterates through the
elements of the array and binds each of those elements to _el_ in the block `{
|el| puts el }`, and then executes the `puts el`. If you need more than a
one-liner, do use `do...end`:
~~~ruby
a.each do |el|
puts el
puts el
end
~~~
Sometimes you want the index as well as the element (mostly in interviews, in
real life not so much). So similar to the trickery we saw before you could do:
~~~ruby
a.length.times { |i| puts a[i] }
~~~
However, the thought police favors `each_with_index`:
~~~none
irb> a.each_with_index { |el, i| puts "#{i}: #{el}" }
0: 100
1: 200
2: 300
~~~
Get those [Anki flashcards](https://apps.ankiweb.net/) out to memorize
the order of `|el, i|`. You did not ask for it, but here's a tip: in Ruby, it often works to
ask "how would I say this out loud?".
!!!
In the array `a`, for _each_ `el` at index `i`, do something.
If your intuition is _for each index `i` which has element `el`_, change your
intuition.
You can loop over a hash with `each`:
~~~none
irb> a = { first: "John", last: "Doe" }
=> {:first=>"John", :last=>"Doe"}
irb> a.each { |key, value| puts "#{key} -> #{value}" }
first -> John
last -> Doe
~~~
Sometimes you only want to loop through the keys or values of a hash. You can
use `keys` and `values`:
~~~none
irb> a.keys
=> [:first, :last]
irb> a.values
=> ["John", "Doe"]
~~~
In contrast to hashes in other languages (e.g. `HashMap` in Java), you _can_
assume an order on key-value pairs. They will be in the order they were
inserted:
~~~none
irb> a = {}
=> {}
irb> a["first"] = 1
=> 1
irb> a["second"] = 2
=> 2
irb> a.each { |key, value| puts "#{key} -> #{value}" }
first -> 1
second -> 2
~~~
Sometimes you can benefit from going one abstraction higher than pure looping,
and use a [`map`](https://docs.ruby-lang.org/en/2.6.0/Array.html#method-i-map):
~~~none
irb> a = [1, 2, 3]
=> [1, 2, 3]
irb> b = a.map { |x| x * 2 }
=> [2, 4, 6]
irb> a
=> [1, 2, 3]
~~~
If you do not want a new array, you can use the [destructive variant
`map!`](https://docs.ruby-lang.org/en/2.6.0/Array.html#method-i-map-21):
~~~none
irb> a.map! { |x| x * 2 }
=> [2, 4, 6]
irb> a
=> [2, 4, 6]
~~~
The convention in Ruby is to append `!` to function names if those functions
are destructive. The exclamation mark is a regular part of the function name.
We can also map hashes. For example, collect all keys of a map, but uppercased:
~~~none
irb> a = { first: "John", last: "Doe" }
=> {:first=>"John", :last=>"Doe"}
irb> a.map { |key, value| key.upcase }
=> [:FIRST, :LAST]
~~~
What if you want to map a `Hash` to a `Hash`, and you want to keep the keys but
uppercase the values?
~~~none
irb> a.map { |key, value| [key, value.upcase] }.to_h
=> {:first=>"JOHN", :last=>"DOE"}
~~~
The `map` returns an array of `[key, value.upcase]` arrays. So I use `to_h` to
convert that array to a hash. Generally:
~~~none
irb> [[1, 2], [3, 4]].to_h
=> {1=>2, 3=>4}
~~~
This is no longer elemental.
# Functions
Functions (or _methods_, when appearing in classes) have this
shape:
~~~ruby
def function_name(arg1, arg2,...)
# do stuff
end
~~~
Functions start with `def` and end with `end`. A function that doubles every
element in an array `a`:
~~~ruby
def double_all(a)
a.map { |x| x * 2 }
end
~~~
"Oh no," you say. "They forgot to type the arguments." Yes. That `a` can be
anything as long as it understands `map`.
Call `double_all`:
~~~none
irb> a = [1, 2, 3]
irb> puts double_all(a)
2
4
6
~~~
If you miss types, or you work in a large code base with many developers and
you prefer sanity over insanity, check out [sorbet](https://sorbet.org/).
Sorbet allows you to annotate functions with type information. For example, add
a signature to `double_all` with the sorbet `sig` annotation:
~~~ruby
sig { params(a: T::Array[Integer]).returns(T::Array[Integer]) }
def double_all(a)
a.map { |x| x * 2 }
end
~~~
This indicates that `double_all` takes in an array of integers `a` and returns
an array of integers.
You can have defaults for your arguments:
~~~ruby
def double_all(a = [1, 2])
a.map { |x| x * 2 }
end
~~~
After which, you can leave off the argument to `double_all`:
~~~none
irb> puts double_all
2
4
~~~
A function with a `Hash` as an argument:
~~~ruby
def hash_keys(h)
puts "a: #{h[:a]}"
puts "b: #{h[:b]}"
end
~~~
And then:
~~~none
irb> puts hash_keys({ a: 1, b: 2 })
a: 1
b: 2
~~~
If a `Hash` is the last argument of the function call, you can leave off the
curly braces:
~~~none
irb> puts hash_keys(a: 1, b: 2)
a: 1
b: 2
~~~
That reminds of _keyword arguments_. Indeed, you can now change the
calling order:
~~~none
irb> puts hash_keys(b: 2, a: 1)
a: 1
b: 2
~~~
# Classes and Methods
## Defining classes
Define a minimal class with the `class` keyword, a name, and `end`:
~~~ruby
class A
end
~~~
This introduces `A` into the world. Here's how to create a new `A` object `a`,
how to ask its `object_id`, how to get its String representation with `to_s`,
and how to get its class:
~~~none
irb> a = A.new
=> #