Try   HackMD

Ruby Guide

Table of Contents


Getting started with Ruby

(This is the first part of a two-part series. Click here to view part two!)


Variables Are Barewords

Scope

$global_var = "this has a global scope!"
value = 10
_private = nil
SPEED_OF_LIGHT = 299_792_458
Begins Scope More Info
$ Global Available everywhere within your ruby script.
@ Instance Available only within a specific object, across all methods in a class instance. Not available directly from class definitions.
@@ Class Available from the class definition and any sub-classes. Not available from anywhere outside.
[a-z] or _ Local Availability depends on the context. You’ll be working with these most and thus encounter the most problems, because their scope depends on a variety of factors.
[A-Z] Constant This is purely a naming convention, and is not enforced.

Built in Data Types

Unlike many languages, Ruby doesn't have primitive types; all Ruby data types are classes:

  • Numbers
  • Strings
  • Symbols
  • Arrays
  • Hashes
  • Nil
  • True
  • False

Numbers

Ruby does not require you to distinguish between types of numbers. Instead, the result of any numeric operation is dependant on the types involved:

puts 3 / 2
# => 1    [int / int = int]

puts 3.0 / 2
# => 1.5    [float / int = float]

puts 3 / 2.0
# => 1.5    [int / float = float]

Strings

Unlike some other languages, all strings in Ruby are mutable, such that they can be modified in place without cloning the value.

Strings concatenate via the + operator — non-String types will not implicitly convert during concatenation.

age = 12

puts "Today is day " + age
# => no implicit conversion of Integer into String (TypeError)

puts "Today is day " + age.to_s
# => Today is day 12

String interpolation via #{} forces implicit type conversion:

## double-quoted strings attempt interpolation
puts "Today is day #{age} and still no word."
# => Today is day 12 and still no word.

## single-quoted strings are considered "literal"
puts 'Today is day #{age} and still no word.'
# => Today is day #{age} and still no word.

N.B. strings starting with the % string-indicator can be delimited by any character, not just quotes (meaning less need to escape special characters)! There exist a variety of string modifiers that change how the string is interpreted: q,Q,i,I,w,W,r,x,s

puts %$This is a string with "quotes" in it!$
# => This is a string with "quotes" in it!

value = "Thursday"

## %Q forces double-quoted context
puts %Q(Today is #{value}, but tomorrow is Friday!)
# => Today is Thursday, but tomorrow is Friday!

## %q forces single-quoted context
puts %q(Today is #{value}, but tomorrow is Friday!)
# => Today is #{value}, but tomorrow is Friday!

Symbols


In Ruby, a symbol is a sort of immutable enumerated value. Consider the following code:

enum Status { OPEN, CLOSED };
Status original_status = OPEN;
Status current_status  = CLOSED;

Because Ruby is a dynamic language, we don't need to declare a type Status nor define the values. Insteads we represent the enumeration values as symbols:

original_status = :open
current_status  = :closed

Much like an enumeration, symbols are a form of reference value: any two symbols with the same name point to the same memory location!

"string".object_id == "string".object_id
# => false

:symbol.object_id == :symbol.object_id
# => true

In Ruby (and Rails), we often use symbols as a sort of human-readable name:

find_speech(:gettysburg_address)

N.B. Because of the way Ruby symbols are registered, they can never be garbage collected. In other words, if we create 10,000 one-off symbols that never get used again, the memory remains allocated for the duration of the runtime. For more info, see this informative post.


Arrays

Arrays in Ruby are heterogeneous — they can contain multiple datatypes!

arr = [1, "two", 3.0] # => [1, "two", 3.0]

To instantiate an array with a collection of values, a block can be passed to the array constructor:

Array.new(4) { Hash.new }  # => [{}, {}, {}, {}]
# initiate an array of length 4,
# and use the function Hash.new()
# as the input for each index position

Array.new(4) {|n| n.to_s } # => ["0", "1", "2", "3"]
# in this above example, the block automatically
# receives the index-iterator position
# as an input argument
# (this is just one of many examples of ruby "magic")

Finally, Ruby provides several useful shortcuts to access Array valeus:

  • Array indexes that are out of bounds return nil.
  • Negative array indexes reference values right-to-left.
  • Two values separated by a comma will return a subset ("slice") of array values—the first digit is the index offset, and the second value is the length of the slice.
  • Finally, the range operator .. can be used to select the values from a sequence of indexes.
arr = [1, 2, 3, 4, 5, 6]

arr[100]  #=> nil
arr[-1]   #=> 6
arr[-2]   #=> 5
arr[2, 3] #=> [3, 4, 5]
arr[1..4] #=> [2, 3, 4, 5]
arr[1..-3] #=> [2, 3, 4]

Hashes (Ruby's Dictionaries)

Like Arrays, Hashes in Ruby can be heterogenousand any object can be used as a key (meaning that they aren't limited to integer or string datatypes).

Hashes derive their name from the hashing mechanism used to distribute their data across memory. For more information, see this informative post.

A Hash can be easily instantiated via the Hash.new function, or by using the implicit form:

grades = Hash.new
grades["Dorothy Doe"] = 9

grades = { "Jane Doe" => 10, "Jim Doe" => 6 }

The => symbol (known as the "fat comma" in many languages) is called a "Hash Rocket" in Ruby, and is used to indicate the relationship between a key/value pair in a Ruby hash declaration.

Afterward, a value is accessed by using the associated key:

puts grades["Jane Doe"] # => 10

Ruby 2.0.0 brought a new syntax shortcut for using symbols as keys, to bring Ruby hashes more in line with JSON stylization:

:symbol = "some key symbol"
old_style = { :symbol => "zero" }
new_style =  { symbol: "zero" }

old_style[:symbol] == new_style[:symbol]
# => true

Keyword Arguments are a Hash-like structure that provide named arguments in function calls:

class Person
    def self.create(params)
        @name = params[:name]
        @age  = params[:age]
    end
end

body = Person.create(name: "John Doe", age: 27)

Operators, Functions, and Methods


Operators

Operator Precendence Table

N.B. notable operators:

  • << — also used as the append (push) operator
  • ..., .. — range operators
  • &&=, ||= — compound assignment
  • <=>, =~, === — equality and matching (RegEx)
  • not, and, or — verbose logical operators

Parallel Assignment

Ruby also supports the parallel assignment of variables. This enables multiple variables to be initialized with a single line of Ruby code.

a = 10
b = 20
c = 30

# this is equivalent:

a, b, c = 10, 20, 30

Operators are Methods Too!

Many core operators are powered by underlying methods:

## When you write
number = 2 + 3 * 4

array = [1, 2, 3]
array[2] = array[3]


## Ruby understands this as
number = 2.+(3.*(4))

array = [1, 2, 3]
array.[]=(2, array.[](3))

Using Safe Navigation

The Safe Navigation operator &. can be used to avoid "No " errors. This is especially useful when calling a method on a variable that is assigned at runtime and could have a nil value.

account = Account.new(owner: nil)

account.owner.address
# => NoMethodError: undefined method `address' for nil:NilClass

account && account.owner && account.owner.address
# => nil

account&.owner&.address
# => nil

N.B.   Sadly, the Safe Navigation operator was not introduced until Ruby v2.3.0


Method Declarations

Methods are declared via a def ... end block. There are two types of arguments that can be declared in a signature: positional and named. All arguments can be given default values via assignment in the signature. Named arguments are indicated by a : suffix, optionally followed by a default value.

def raise(x, y = 1)
    return x ** y
end

raise(4,2) #=> 16
raise(4) #=> 4


def raise(value:, exp: 1)
    return :value ** :exp
end

raise(value: 4, exp: 2) #=> 16
raise(value: 4) #=> 4

Unnamed arguments can be made optional by a * suffix. Additionally, an unnamed-style signature can be made to "slurp" up all remaining arguments via a prefix splat operator:

## optional arguments using '*'
def add(x, y*)
    if (y.nil?)
        return x
    else
        return x += y
    end
end

## "greedy" splat-signatures will slurp all remaining arguments
def add(x, *y)
    for i in y do
        x += i
    end
    
    return x
end

For named signatures, a ** splat prefix will slurp all named arguments passed into a method, even if the names do not appear in the signature:

def do_stuff(name:, **options)
    return options
end

do_stuff(name: "Zix", size: 9001, status: "very cool")
# => {size: 9001, status: "very cool"}

This is particularly useful for interacting with varying run-time input and NoSQL-type data storage.


The Versatile Splat Operator

The so-called "Splat" operator (*) is used to decompose an array or hash in-place, such that the values are passed as an inline list.

In other words, a Splat converts an array (or hash) into a list:

arr = [1,2,3]
puts *arr  #=> iterates across 'arr' and prints each value
# 1
# 2
# 3

struct = {foo: 'bar', bat: 'baz'}
puts *struct  #=> iterates across the key-value *pairs* of 'struct'
# [:foo, 'bar']
# [:bat, 'baz']

The Splat operator can be used to "smear out" an iterable structure into its individual components. However, the Splat also has a second, more powerful use:


Argument Slurping

When a Splat is used in a method's signature, the method is said to "slurp" arguments.

To understand slurping, consider the following function:

def print_recipe(name, ingredients)
    puts "this is my awesome #{name} recipe! you'll need: #{ingredients}"
end

Here, you can print the ingredients for a recipe, but all of the ingredients must be encapsulated in a single object:

print_recipe('cake', 'flour')
#=> 'this my awesome cake recipe! You'll need: flour'

print_recipe('cake', ['eggs', 'flour', 'butter'])
#=> 'this my awesome cake recipe! You'll need: ['eggs', 'flour', 'butter']'

As expected, the array is passed as a positional argument. This is fine, but what if you need to call your function with flat list of arguments?

print_recipe('cake', 'eggs', 'flour', 'butter')
#=> ArgumentError (wrong number of arguments (given 4, expected 2))

Don't worry, there's good news! In the context of an method signature, the Splat operator cross-composes the arguments into a single variable containing all of the arguments not explicitly consumed by other parts of the signature:

def print_recipe(name, *ingredients)
    puts "this is my awesome #{name} recipe! you'll need: #{ingredients}"
end

print_recipe('cake', 'eggs', 'flour', 'butter')
#=> this is my awesome cake recipe! you'll need: ["eggs", "flour", "butter"]

Now, you might be asking why this is an advantage. The primary value of the positional-slurp argument is that it allows functions to cope with multiple input-cases:

def vector_squared(*values)
    puts values.map { |x| x*x }
end

vector_squared(2) #=> 4
vector_squared(1,2) #=> 1, 4
vector_squared(3,3,4) #=> 9, 9, 16

However, the real vaue of the slurping-splat comes out when use keyword arguments!


Keyword Slurping

Just like positional-argument slurping, splats can be used to slurp Keyword Arguments into a single structure. This allows a function to incorporate an indefinite space for consuming keywords:

def fancy_function(name, *values, output: false, **options)
    if output
        puts "my name is #{name}!"
        puts "my inputs are: #{values}"
        puts "you configured the following options: #{options}"
    end
end

When a "double-splat" (**) is invoked in a function signature, the indicated argument variable becomes a bucket for all keyword-arguments passed to the function that are not explicitly defined:

fancy_function('foo', 1,2,3, output: true, test: 'case', demo: true)
# my name is foo!
# my inputs are: [1, 2, 3]
# you configured the following options:  {:test=>"case", :demo=>true}

Thus, the double-splat slurping operator opens the door for a whole host of powerful dynamic functions, which can be adapted at run time to suit a situational need!

N.B. slurping keyword-arguments should ALWAYS come at the end of the signature, to avoid slurping explicitly defined keyword arguments.


Return Values for Functions

All functions result in an implicit return. If you do not specify a return value, then the function will return the last evaluated expression.

def increment(x)
    return x = x + 1
end

def double(y)
    y *= 2
end

puts increment(7)
# => 8

puts double(9)
# => 18

Suffix Methods

By default, object methods clone the target instance and then perform an action on and return the copy. Bang Methods perform an action directly on an object, returning the modified object as a result.

name = "Ruby Monsters"
puts name.downcase  #=> ruby monsters
puts name  # => Ruby Monsters


name = "This and That"
puts name.downcase!  # => "this and that"
puts name  # => "this and that"

Use bang methods with caution! In-place modifications can be dangerous, and should generally be avoided unless absolutely needed.

An important Ruby naming convention says that any method which returns a boolean value (known as a predicate method) should end in a ?.

"foo".empty?
# => false

"".blank?
# => true

Truthiness and Boolean Logic

Ruby is a "truthy" language: any datatype can be evaluated in a boolean context!

Truthiness Table


Flow Control and Blocks

In Ruby nearly everything is an expression

if num > 0
    puts "number is positive"
elsif num == 0
    puts "number is zero"
else
    puts "number is negative"
end


coin = if rand() > .5
then
    "head"
else 
    "tails"
end


schedule = case day_of_week
when 'Sunday'
    puts 'Closed'
when 'Satusrday'
    puts 'Open 2-5'
else 
    puts 'Open 9-5'
end

Block Structures

Blocks (sometimes known as anonymous functions) are code between braces {} or verbose delimiters do...end. Arguments for inline-iterators (map, filter, each, etc.) are declared using pipes (|x|), which appear immediately after the opening brace.

The following statements are equivalent:

for i in 0..9 do
    puts i
end

(0..9).each { |i| puts i }

# this could also be written as:
# (0..9).select(i => print i)

Blocks as Arguments

Some functions take a block as an argument. However, Ruby convention dictates that we omit the parenthesis in this context

The map function operates on the contents of an enumerable datatype via an implicit iterator:

puts (0..9).map { |i| i*i }
## or its synonym
puts (0..9).collect { |i| i*i }
# => [0,1,4,9,16,25,36,49,64,81]

Iterator methods can also be chained together for complex ETL:

coin_tosses = (0..9).map { rand() } 
    .map { |num| if num > 0.5 then "head" else "tails" end } 
    .each { |coin| puts coin }
	
# (0..9).map {...}.map {...}.each{...}

The select function returns only the elements for which the provided block returns true:

coin_tosses.select { |coin| coin == "head" }.count
 ## or its synonym
coin_tosses.filter { |coin| coin == "head" }.count

You can also act on the contents of an enumerable using one of the variants of the [].each iterator. This is used when you don't want to modify the enumerable object.

(0..5).each do |x|
    print x
end
# => 012345


(4..1).each_index { |i| print i, " -- " }
# => 0 -- 1 -- 2 -- 3 --

Loop Control

TODO
https://www.geeksforgeeks.org/ruby-loops-for-while-do-while-until/ http://zetcode.com/lang/rubytutorial/flowcontrol/


Postfix statements

Conditional statements and even loops can be defined after a block, in a style known as "postfix".

raise ArgumentError, "Can't be less than 100" unless num > 100

puts i++ while i < 10

N.B. the unless... statement is equivalent to if not...


Exceptions

Ruby handles exceptions much like other languages; most useful exceptions derive from StandardError, and each Exception object is associated with a message string and a stack-trace.

Like everything in Ruby, exceptions are objects, and have an inheritance as follows:

Exception
   │
   ├── fatal   [used internally by Ruby]
   │
   ├── NoMemoryError
   ├── ScriptError
   │    ├── LoadError
   │    ├── NotImplementedError
   │    └── SyntaxError
   │
   ├── SecurityError
   ├── SignalException
   │    └── Interrupt
   │
   ├── StandardError
   │    ├── ArgumentError
   │    ├── StopIteration
   │    ├── IndexError
   │    │    ├── KeyError
   │    │    └── StopIteration
   │    │
   │    ├── IOError
   │    │    └── EOFError
   │    │
   │    ├── LocalJumpError
   │    ├── NameError
   │    │    └── NoMethodError
   │    │
   │    ├── RangeError
   │    │    └── FloatDomainError
   │    │
   │    ├── RegexpError
   │    ├── RuntimeError
   │    ├── SystemCallError
   │    │    └── [system-dependent exceptions (Errno::xxx)]
   │    │
   │    ├── ThreadError
   │    ├── TypeError
   │    └── ZeroDivisionError
   │
   ├── SystemExit
   └── SystemStackError

Throw Catch

In keeping with Ruby tradition, the usual exception-handling terminology of Try...Throw...Catch has a slightly different meaning then you might expect.

Instead of error-handling, Catch blocks act as a sort of anonymous function, using throw to denote the return value:

def index_of(target, list)
  index = catch(:index) {
    for pos in 0..list.length do
      if target == list[pos]
	    throw :index, pos
	  end
    end
    "NaN"
  }
  puts "#{target} in #{list} appears at position #{index}" unless index == "NaN"
end

This tortured example demonstrates the general (mis)use of throw; more often, a fully broken out function will be safer, easier to read, and more reusable than a Catch block.


Begin Rescue

Instead of the usual Try...Throw...Catch block, exceptions are handled via Begin...Raise...Rescue:

def raise_and_rescue  
  begin
    puts 'I am before the raise.'
    raise StandardError.new('An error has occured!')
    puts 'I am after the raise.'
  rescue OneTypeOfException => e
    puts "This is the message from an exception: #{e}"
  rescue AnotherTypeOfException => e
    puts "I am another rescue: #{e}"
  else
    puts 'I only print if no exception occured.'
  ensure
    puts 'I will always run, with or without an exception.'
  end
end

Classes, Polymorphism, and Composition


Classes and Inheritance

TODO [inheritance, private, attributes, self]


Mix-in Modules

Modules are essentially static classes: they hold methods just like classes, but cannot be instantiated. This is useful if we have methods that we want to reuse in certain classes, but also want to keep them in a central place, so we do not have to repeat them everywhere. Modules can be utilized in a class via the include statement:

class Chocolate
  include IceCream
end

class Vanilla
  include IceCream
end

module IceCream
  def flavor
  	...
  end
end

cone = Vanilla.new
puts cone.flavor

When a Module is injected into a class this way, the Module can be thought of as a sort of Superclass known as a "mix-in". This paradigm (composition over inheritance) is generally preferred by the Ruby community, to avoid leaky abstractions and overlapping inheritance.


Additional Notes


Parenthesis

You will often see Ruby code that seems to be missing parenthesis. In Ruby, when you define or call (execute, use) a method, you can omit the parentheses entirely.

In fact, you have already seen us use the puts method this way throughout the presentation.

puts("this has parens")
# => "this has parens"

puts "this does not"
# => "this does not"

There is no clear rule about this, but there are some conventions:

  • use parentheses for all method calls that take arguments, except for the methods puts and p, require, and include.
  • If a method does not take any arguments, then omit the empty parentheses.
bad = Array.new()
good = Array.new

puts "Missing parens?".concat " Don't do it."
# => Missing parens? don't 
puts "Parens".concat(" are the best!")
# => Parens are the best!

Ruby Ecosystem


Managing Ruby Versions

TODO [ asdf ]


Ruby Gems

Libraries and packages in Ruby are known as Gems, and are managed via the RubyGems package manager. Zix maintains its own Gem server that provides a number of proprietary, in-house gems, used throughout the Zix codebase.

By default ruby gems are installed globally.

Gems are installed/removed via a simple command:

$ gem [un]install some_gem

Bundler

Running applications that use different versions of dependencies can cause big problems because everything is installed globally. Bundler helps to manage dependencies. It gives us the Gemfile, Gemfile.lock and helps us to run commands in the "context" of our application.


IRB

Ruby provides a REPL console environment called the Interactive Ruby Shell:

$ irb

When working within a Rails application, you should instead access the IRB via bundle exec:

$ bundle exec rails console
# Or just
$ bundle exec rails c

Rake

Rake is a taskrunner. It does things like backup your database, run tests, and generate reports.


CONTINUED IN PART 2

Read on to learn about Ruby..ON RAILS!