(This is the first part of a two-part series. Click here to view part two!)
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. |
Unlike many languages, Ruby doesn't have primitive types; all Ruby data types are classes:
Ruby does not require you to distinguish between types of numbers. Instead, the result of any numeric operation is dependant on the types involved:
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.
String interpolation via #{}
forces implicit type conversion:
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
In Ruby, a symbol is a sort of immutable enumerated value. Consider the following code:
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:
Much like an enumeration, symbols are a form of reference value: any two symbols with the same name point to the same memory location!
In Ruby (and Rails), we often use symbols as a sort of human-readable name:
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 in Ruby are heterogeneous — they can contain multiple datatypes!
To instantiate an array with a collection of values, a block can be passed to the array constructor:
Finally, Ruby provides several useful shortcuts to access Array valeus:
offset
, and the second value is the length
of the slice...
can be used to select the values from a sequence of indexes.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 informative post.
A Hash can be easily instantiated via the Hash.new
function, or by using the implicit form:
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:
Ruby 2.0.0 brought a new syntax shortcut for using symbols as keys, to bring Ruby hashes more in line with JSON stylization:
Keyword Arguments are a Hash-like structure that provide named arguments in function calls:
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
Ruby also supports the parallel assignment of variables. This enables multiple variables to be initialized with a single line of Ruby code.
Many core operators are powered by underlying methods:
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.
N.B. Sadly, the Safe Navigation operator was not introduced until Ruby v2.3.0
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.
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:
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:
This is particularly useful for interacting with varying run-time input and NoSQL-type data storage.
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:
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:
When a Splat is used in a method's signature, the method is said to "slurp" arguments.
To understand slurping, consider the following function:
Here, you can print the ingredients for a recipe, but all of the ingredients must be encapsulated in a single object:
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?
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:
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:
However, the real vaue of the slurping-splat comes out when use keyword arguments!
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:
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:
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.
All functions result in an implicit return
. If you do not specify a return value, then the function will return the last evaluated expression.
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.
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 ?
.
Ruby is a "truthy" language: any datatype can be evaluated in a boolean context!
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:
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:
Iterator methods can also be chained together for complex ETL:
The select
function returns only the elements for which the provided block returns true:
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.
TODO
https://www.geeksforgeeks.org/ruby-loops-for-while-do-while-until/ http://zetcode.com/lang/rubytutorial/flowcontrol/
Conditional statements and even loops can be defined after a block, in a style known as "postfix".
N.B. the
unless...
statement is equivalent toif not...
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:
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:
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
:
TODO [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:
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.
There is no clear rule about this, but there are some conventions:
puts
and p
, require
, and include
.TODO [
asdf
]
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:
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:
When working within a Rails application, you should instead access the IRB via bundle exec
:
Rake is a taskrunner. It does things like backup your database, run tests, and generate reports.