title | description | tags |
---|---|---|
Ruby on Rails Training (part 1) – Ruby |
Overview of the "Ruby" part of Ruby on Rails |
ruby, ruby on rails, coding, language, tutorial, learning |
TBD
$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 various things. |
[A-Z] |
Constant | This is purely a naming convention, and is not enforced. |
Unlike many languages ruby doesn't have primitive types; all Ruby data types are classes:
- Numbers
- Strings
- Symbols
- Arrays
- Hashes
- Nil
- True
- False
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]
Unlike some other languages, all strings in Ruby are mutable, such that they can be modified in place without cloning the old value.
Strings concatenate in Ruby just as in C# — non-String types will not implicitly convert during concatenation. However, string interopation via #{}
forces implicit type conversion:
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
# double-quoted strings attempt interpolation
# single-quoted strings are considered "literal"
puts "Today is day #{age} and still no word."
# => Today is day 12 and still no word.
puts 'Today is day #{age} and still no word.'
# => Today is day #{age} and still no word.
Finally, strings can be delimited by any character, not just quotes (meaning less need to escape special characters!)
puts %$This is a string with "quotes" in it!$
# => This is a string with "quotes" in it!
value = "Thursday"
# %Q forces double-quoted context
# %q forces single-quoted context
puts %Q{Today is #{value}, but tomorrow is Friday!}
# => Today is Thursday, but tomorrow is Friday!
puts %q(Today is #{value}, but tomorrow is Friday!)
# => Today is #{value}, but tomorrow is Friday!
In Ruby, a "symbol" is a sort of immutable enumerated value. Consider the following C# code:
enum Status { OPEN, CLOSED };
Status original_status = OPEN;
Status current_status = CLOSED;
Because Ruby is a dynamic language, we don't worry about declaring a Status type, or keeping track of the legal values. Instead, we represent the enumeration values as symbols:
original_status = :open
current_status = :closed
Much like an enumeration, symbols are a form of reference: any two symbols with the same name point to the same data in memory!
"string".object_id == "string".object_id
# => false
:symbol.object_id == :symbol.object_id
# => true
In Ruby and Rails, we often use symbols when referring to things by name:
find_speech(:gettysburg_address)
[color=#FF0000] N.B. Because of the way Ruby symbols are stored, they can never be garbage collected. So if we create 10,000 one-off symbols that we'll never use again, we'll never get the memory back. For more info, see this post.
Ruby Arrays are heterogeneous — they can contain multiple datatypes!
arr = [1, "two", 3.0] # => [1, "two", 3.0]
To instantiate an array with an array of values, a block can be passed to the array constructor:
Array.new(4) { Hash.new } # => [{}, {}, {}, {}]
Array.new(4) {|i| i.to_s } # => ["0", "1", "2", "3"]
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 of array values determined by the offset and length arguments.
- Finally, the range operator
..
can be used to select the values found at several 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]
Like Arrays, Hashes in Ruby can be heterogenous...and 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 blog 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
Hashes are also commonly used as a way to have named parameters in functions.
class Person def self.create(params) @name = params[:name] @age = params[:age] end end body = Person.create(name: "John Doe", age: 27)
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
N.B. notable operators:
<<
— append (push) operator...
,..
— range operators&&=
,||=
— compound assignment<=>
,=~
,===
— equality and matching (RegEx)not
,and
,or
— verbose logical operators
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
If you write
number = 2 + 3 * 4
array = [1, 2, 3]
array[2] = array[3]
Then Ruby will translate this to the following:
number = 2.+(3.*(4))
array = [1, 2, 3]
array.[]=(2, array.[](3))
Prevents "NoMethod" 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 without an owner
account.owner.address
# => NoMethodError: undefined method `address' for nil:NilClass
account && account.owner && account.owner.address
# => nil
account&.owner&.address
# => nil
[color=#FF0000] N.B. sadly, the Safe Navigation operator is only available in Ruby v2.3.0 and above.
Methods are declared via a def ... end
block. There are two types of arguments that can be declared in a signature: named, and unnamed. Unnamed 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
def raise(value:, exp: 1)
return :value ** :exp
end
Unnamed arguments can be made optional by a *
suffix. Additionally, a signature can be made to "slurp" up all remaining arguments via a *
prefix:
def add(x, y*)
if (y.nil?)
return x
else
return x += y
end
end
def add(x, *y)
for i in y do
x += i
end
return x
end
Finally, a **
argument 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"}
All methods contain an implicit return
. If you do not specify a return value, then the method will return the last evaluated expression.
def increment(x)
return x = x + 1
end
def double(y)
y *= 2
end
num = 7
puts increment(num)
# => 8
puts double(num)
# => 16
One common naming convention in Ruby is that any method that returns a boolean should end in a ?
:
"foo".empty?
# => false
"".blank?
# => true
Standard methods perform an action on a copy of the object and then return that copy.
name = "Ruby Monsters"
puts name.downcase
# => ruby monsters
puts name
# => Ruby Monsters
[maybe condense this?]
Bang methods perform an action directly on an object, returning the modified object as a result.
name = "AppRiver + Zix"
puts name.downcase!
# => appriver + zix
puts name
# => appriver + zix
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
raise ArgumentError, 'Number cannot be greater than 100' if num > 100
unless ...
is the same asif not ...
case day_of_week
when 'Sunday'
puts 'Closed'
when 'Saturday'
puts 'Open 2-5'
else
puts 'Open 9-5'
end
Blocks (called closures in other languages) are code between braces {}
or do..end
.
The following statements are equivalent:
for i in 0..9 do
puts i
end
(0..9).each() { |i| puts i }
# C# style:
# (0..9).select(i => print i)
The synonyms collect
and map
are like the map
function in most other languages.
puts (0..9).collect { |i| i*i }
puts (0..9).map { |i| i*i }
# => [0,1,4,9,16,25,36,49,64,81]
These methods can also be chained:
(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 is like filter
in most other languages:
number_of_heads = (0..9).map { rand() }
.map { |num| if num > 0.5 then "head" else "tails" end }
.select { |coin| coin == "head" }
.count
[].each do
block:
for i in 0..9 do
puts i
end
(0..9).each do |i|
puts i
end
# C# style:
# (0..9).select(i => print i)
[ SEE: https://www.geeksforgeeks.org/ruby-loops-for-while-do-while-until/ ] [ SEE: http://zetcode.com/lang/rubytutorial/flowcontrol/ ]
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
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.
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
[inheritance, private, attributes,
self
]
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.
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
bad = "Missing parens?".concat " Don't do it."
good = "Parens".concat(" are the best!")
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
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.
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 rails console
# Or just
$ bundle exec rails c
Rake is a taskrunner. It does things like backup your database, run tests, and generate reports.