~tsvallender

My name is Trevor, I’m a software engineer specialising in Ruby on Rails. I’m also a dad, geek and tabletop gamer.

Profile photo of Trevor Vallender

Ruby

19 January 2022 | Updated on

This is my overview of Ruby, aiming to give a bird’s eye view of the language. My goal is to present information in such a way that it will prove as a useful introduction to Ruby for programmers coming from other languages, and as a reference for relatively new Ruby developers. It is not an introduction to programming, nor is it in any way exhaustive. It should be used as a place to jump off from and dive deeper through other sources whenever you reach an area you wish to know more about.

Variables

Setting variables

name = "T S Vallender"

Types

There is no need to declare a variable’s type.

Numbers

Operators
5 + 5 # 10
5 - 2 # 3
5 * 5 # 25
10 / 5 # 2
3 ** 3 # 27 (Exponent)
10 % 5 # 0 (Modulo)

Ruby uses standard mathematical order of operation (PEDMAS/BODMAS).

Numeric types

Floats are an imprecise data type due to the way they are stored and decimal should be used when precision is necessary. Rational is also available for dealing with rational numbers, but care must be taken not to treat an irrational number as rational.

0.2 + 0.1 == 0.3 # false due to float imprecision
BigDecimal("0.2") + BigDecimal("0.1") == 0.3 # true

Strings

Strings are defined between double or single quotes. Doubles allow for string interpolation, singles do not.

String interpolation
puts "I can insert #{my_var} into a string"

Any valid Ruby can be interpolated, often used to give an optional default value with a conditional.

String manipulation

Some common string manipulation methods:

name = "T S Vallender"
name.upcase # "T S VALLENDER"
name.downcase # "t s vallender"
name.reverse # "rednellaV S T"

poem = "And the Raven, never flitting, still is sitting, still is sitting"
poem.sub "sitting", "standing"
# "And the Raven, never flitting, still is standing, still is sitting
poem.gsub "sitting", "standing"
# "And the Raven, never flitting, still is standing, still is standing
# Note these do not change the original value - you need sub! and gsub! for that.

quote = "  Quoth the Raven “Nevermore.   "
quote.strip! "Quoth the Raven “Nevermore."
quote.split # ["Quoth", "the", "Raven", "Nevermore"]

Arrays

Arrays are an Enumerable type.

arr = [3, 'Three', 8, 3, true]
arr.delete 3 # ['Three', 8, true]
arr.delete_at 0 # [8, true]

fellowship = ['Gimli', 'Pippin', 'Aragorn', 'Frodo', 'Pippin' ]
fellowship.delete_if { |x| x.length > 5 }
# ['Gimli', 'Aragorn']
fellowship.join(' and ') # "Gimli and Aragorn"

Items can be pushed to an popped from arrays with .push and .pop. .pop returns the value popped.

nil values are added to any empty spaces in an array.

Arrays of strings can also be declred using the below syntax

arr = %w(One Two Three Four)
Common methods
arr = ["a", "b", "c"]
arr.size # 3

Hashes

Hashes are another Enumerable type. They are a key: value collection.

fellowship = { boromir: "Human", gimli: "Dwarf", legolas: "Elf",
               aragorn: "Human", frodo: "Hobbit" }
fellowship[:gimli] # "Dwarf"
fellowship.delete :boromir
fellowship.each_key { |x| puts x }
fellowship.each_value { |x| puts x }

fellowship[:gandalf] = 'Maiar' # add a new key-value pair.
fellowship.invert # flips keys and values

more = { pippin: "Hobbit", merry: "Hobbit", sam: "Hobbit" }
fellowship.merge more # combine hashes

fellowship.keys # array of keys
fellowship.values # array of values

This is the modern syntax. Ruby converts these internally to symbols. Older syntaxes include { "gimli" => "Dwarf" } and { :gimli => "Dwarf" }.

Scope

Local variables
Local variables are limited to the scope in which they are declared (e.g. a method or loop).
Global variables
Global variables are defined with a leading $. They are generally a bad idea.
Instance variables
Instance variables are defined with a leading @ and are available to a given instance of a given object.
Class variables
A class variables are defined with two leading @s and are available to all instances of a class. They are rarely used.
Constants
Constants are defined with an initial capital (generally in all caps. They *can be changed*, Ruby will just issue a warning if you do so.

Conditionals

Ruby supports the standard conditional operators ==, !=, <, >, <= and <=.

Compound conditionals are supported with && and ||. Priority can be enforced with parentheses. Ruby also gives and and or, which have lower precendence.

if x < y
  puts "#{x} is bigger"
elsif x > y
  puts "#{y} is bigger"
else
  puts "They are equal!"
end

# conditionals can also go on the end:

puts "They are equal" if x == y

arr = [1, 2, 3]
unless arr.empty?
  puts arr
end

puts arr unless arr.empty?

## Ruby also supports the ternary operator
x = y > z ? y : z # sets x to the larger of y or z

Iterators & Loops

While

while true
  p "Infinite loop"
end

Each

The each method is available on any collection of items

[1, 2, 3].each do |x|
  p x # x is the element
end

[1, 2, 3].each { |x| p i } # equivalent to above

# If an index is required:

[1, 2, 3].each_with_index do | x, i |
  p "Index: ", i, "Value: ", x
end

With a hash, .each use:

my_hash.each do |key, value|
  p key, value 
end

For in

Rarely used, .each is more common.

for i in 0..10
  p i # prints 0 to 10
end

Methods

def my_method
  puts "This is my method"
end

Methods in Ruby return the value of their final statement. return is generally only used to return early.

Naming methods

Methods are generally defined in snake_case.

Methods with a trailing ! are “dangerous”, such as those in the core usually modifies its receiver.

Class vs. instance methods

Class methods are defined with a leading self.. Instance methods can only be called on an instance of a class.

Arguments

def say_hello(name)
  puts "Hello #{name}!"
end

say_hello("Trevor") # "Hello Trevor!"

Parentheses are optional both when defining the function and calling it:

def say_hello name
  puts "Hello #{name}!"
end

say_hello "Trevor" # "Hello Trevor!"

Named arguments

def say_hello name:, place:
  puts "Hello #{name} in #{place}!"
end

say_hello name: "Trevor", place: "UK" # Hello Trevor in UK

Default arguments

def say_hello name:, place: "UK"
  puts "Hello #{name} in #{place}!"
end

say_hello name: "Trevor" # Hello Trevor in UK

Collections as arguments

A splat argument allows multiple arguments to be treated as an array.

def make_array *species
  species
end

make_array "Dwarf", "Elf", "Halfling" # [ "Dwarf", "Elf", "Halfling"]

A keyword-based splat argument takes a hash value instead

def identify **characters
  characters.each do |species, name|
    puts "#{name} is a #{species}
  end
end

c = { "Dwarf": "Gimli", "Elf": "Legolas" }
identify c
# Gimli is a Dwarf
# Legolas is a Elf

Optional arguments

Optional arguments use an empty options hash.

def stats options={}
  puts options[:optional]
end

stats optional: "HEY!"

Procs and Lambdas

Procs and lambdas are encapsulations of a piece of code in a variable.

Procs and lambdas are closures and thus remember they retain the context in which they were created.

Procs

my_proc = Proc.new { |x| x ** x }
my_equiv_proc = proc { |x| x ** x }
another_equiv_proc = Proc.new do |x|
  x ** x
end

my_proc.call(3) # 27
my_proc.(3) # 27
my_proc[3] # 27

Lambdas

my_lambda = lambda { |x| x ** x }
my_lambda[3] # 27

# "Stabby lambda" syntax:
my_equiv_lambda = ->(x) { x ** x}

Differences

Enumerators

Select

Select returns those elements of a collection which meet a given criteria.

(1..20).to_a.select { |x| x.even? }
(1..20).to_a.select(&:even?) # equivalent

Map

Map takes a collection and performs an operation on each one of its members.

arr = ["1", "2", "10"]
arr.map(&:to_i) # [1, 2, 3]

This can be used to create a hash of a value with an associated value, for example strings with their length:

arr = %w(Aragorn, Frodo, Gimli, Gandalf)
Hash[arr.map { |x| [x, x.length] }]

Inject/Reduce

Inject and reduce are aliases. They combine a collection by the repeated application of a binary operator. For example, to sum all elements of an array:

[4, 3, 6, 2].reduce(&:+) # 15

If passed a code block, each element in the enumerable object is passed an accumulator and the element.

[fellowship = ['Gimli', 'Pippin', 'Aragorn', 'Frodo' ]

fellowship.inject do | longest, current |
  current.length &gt; longest.length ? current : longest
end

# Aragorn

Symbols

A symbol is a number with an attached identifier, which is a series of characters or bytes. They are represented in Ruby with a leading colon.

Symbols should be used whenever a textual representation of a discrete set of options is required. If you are dealing with more freeform text, use strings.

Ruby will often convert between symbols and strings for the programmer’s convenience. If possible, you should supply the correct type initially, however, for the best performance.

Working at the console

Writing to the console

puts name # returns nil
p name # returns value

Note also that puts will pretty-print, while p will print arrays including commas, brackets etc., strings wrapped in quotation marks and is thus generally less suitable for use with end users.

Receiving input at the console

name = gets.chomp # chomp removes trailing newline

Object-oriented Ruby

Everything in Ruby is an object.

Classes are named with CamelCase.

class Humanoid
  attr_accessor :name, :address # creates getters and setters
  # initializer method run when new objects are created
  def initialize name:, address:
    @name = name
    @address = address
  end

  def location
    puts "#{name} lives in #{address}"
  end

  def self.fact # Class method
    puts "Humanoids have two legs"
  end
end

class Hobbit &lt; Humanoid
  def initialize name:, address: "The Shire"
    super # Calls parent method
  end
  
  def self.fact
    puts "Hobbits have hairy feet"
  end
end

bilbo = Hobbit.new name: 'Bilbo'
p bilbo.name, bilbo.address
bilbo.location
Hobbit.fact
aragorn = Humanoid.new name: "Strider", address: "Gondor"
Humanoid.fact

Methods below a private keyword are only accessible from the given class. Note there are ways around this but doing so is bad practice. Protected methods are also unavailable outside the class, but while within that class may be called on objects. They are uncommon and private should be used where possible.

The filesystem

f = File.new '/path', 'w+'
f.puts "contents"
f.close

File.open '/path', 'w+' do |f| # second option is mode 
  f.puts("contents")
end

File.write '/path', "contents"

content = File.read '/path' # reads as string

File.delete '/path'

Errors

begin
  puts 10 / 0
rescue ZeroDivisionError => e
  puts e
end

Note you should deal with the error in the initial begin block, rescue should only be for dealing with the error object itself.