This post is part of our ‘The Miners’ Guide to Code Crafting’ series, designed to help aspiring developers learn and grow. Stay tuned for more!
If you’ve reached this post, you’re likely discovering and exploring the Ruby language, so, this article was made for you! The idea here is to introduce the language with lots of practical examples… Hands-on is what we like, right!?
Before we start with the practical part, a little context and theory can be useful, so here are some facts:
- Ruby was officially released in 1995 and initially became popular in Japan;
- It was created by Yukihiro "Matz" Matsumoto who mixed parts of his favorite languages (Perl, Smalltalk, Eiffel, Ada, and Lisp) to form a new language, Ruby;
- In 2006, Ruby reached mass reach, having a very active community in several regions of the world;
- Much of the growth can be attributed to the popularity of the web framework Ruby on Rails.
Diving into the shallow waters of the language, learning that Ruby tends to be as natural as possible, not necessarily simple. Like other languages, Ruby can also have dependencies, which are not known by Ruby Gems. And to manage these dependencies we use the Bundler.
In Ruby, everything is an object. I really mean everything. It includes the primitive types common in other languages such as numbers
, booleans
, and other types. Doubts!? Then run your irb
and let’s get started!
1.class # => Integer
Integer.superclass # => Numeric
Numeric.superclass # => Object
Object.superclass # => BasicObject
BasicObject.superclass # => nil
When we call the object’s superclass method (.superclass
), we get an instance of the parent class as a return. However, BasicObject returns nil
because it is the parent class of all classes in the language. Cool, isn’t it!? Well, the last thing I have to tell you before we dive deeper into the practices is that: Ruby is not Rails!
Knowing that everything in Ruby is an object, Object Orientation is a fundamental concept for using the language as a Ruby developer, and based on this, the method self
returns the instance of the object itself, (similar to this
in JavaScript). Another little-known tip is the use of _
, which is a little different from self
, returning the last object in memory. Don’t worry, we’ll have an example for this!
Ruby is a very flexible language because it allows parentheses ()
to be optional, as well as the declaration of return
in every method (also known as functions in other languages). In Ruby, the last line of the method is automatically returned, and this can cause some confusion for those who are just starting…
Similar to the Rails community, which follows convention over configuration, Ruby uses the Snake Case conversion for naming its files, and Pascal Case for classes and modules, and the parentheses, although optional, are often used in method invocations to improve the readability of the code.
# BAD
SomeClass.rb
# GOOD
some_class.rb
def sum(a, b)
a + b
end
# BAD
sum 1, 2 # => 3
# GOOD
sum(1, 2) # => 3
Still dealing with object-orientation, the language makes it possible to use hierarchy, inheriting classes with ease, as in the following example:
class Animal
# ...
end
class Dog < Animal
# ...
end
We can also define modules to group useful resources together.
module Mathematic
def sum(a, b)
a + b
end
# ...
end
class Calculator
include Mathematic
# ...
end
Calculator.new.sum(5, 3) # => 8
By including a module, we can invoke functions contained in it, and we can easily share a library of resources among developers of the same project. It is worth remembering that ‘modules’ and ‘gems’ are similar, but are used in different contexts, and if you have heard of ‘concerns’, know that this is a Rails thing!
Data Typing and Built-in Methods
In Ruby, objects are dynamically typed, that is, there is no need to declare the type of a variable. Despite this, applying good practices is always recommended, as is the case with the definition of constants, which are declared in uppercase letters.
# CONSTANT
PI = 3.14
# VARIABLE
number = 1
text = 'hello world'
arr = [1, 2, 'c', 'd']
In addition, to the most common data types, Ruby implements the Symbol
type, which is an immutable and unique string, reserving the same memory address for all occurrences of the same symbol. See the following case:
class User
attr_accessor :role
def initialize(role)
@role = role
end
end
# Define roles using symbols and strings
roles_with_symbol = { admin: 'administrator', editor: 'edit-only', viewer: 'read-only' }
roles_with_string = { 'admin' => 'administrator', 'editor' => 'edit-only', 'viewer' => 'read-only' }
# Create user objects with string roles
user_a_str = User.new('admin')
user_b_str = User.new('admin')
user_a_str.role == user_b_str.role # => true
user_a_str.role.object_id == user_b_str.role.object_id # => false - different string objects
# Create user objects with symbol roles
user_a_sym = User.new(:admin)
user_b_sym = User.new(:admin)
user_a_sym.role == user_b_sym.role # => true
user_a_sym.role.object_id == user_b_sym.role.object_id # => true - same symbol object
For beginner developers, the use of Symbol
may seem too abstract and even useless, but as you delve deeper into the language, you will realize that the use of Symbol
is very common in Ruby, especially in Rails, where it can be associated with any data structure, such as hashes, arrays, objects, and, as a result, improve the performance of the application by reusing the same object in memory.
It is worth noting that a Symbol
is not a Singleton, after all, the symbol is immutable, while the singleton is not. A great use case for symbols is to define Enums. Enums can be a set of options, which even when used in several objects/contexts, always represent the same value.
But returning to the point that everything in Ruby is actually an object, and that, whether assigning variables or not, objects in Ruby are instances of classes that have their own methods and attributes made available by the language, let’s look at the example of the class Integer
:
# Addition
1 + 1 #=> 2
# Subtraction
2 - 1 #=> 1
# Multiplication
2 * 2 #=> 4
# Division
10 / 5 #=> 2
# Exponent
2 ** 2 #=> 4
3 ** 4 #=> 81
# Modulus (find the remainder of division)
8 % 2 #=> 0 (8 / 2 = 4; no remainder)
10 % 4 #=> 2 (10 / 4 = 2 with a remainder of 2)
source code: Basic Data Types | The Odin Project
Note that the operations described in the code above are composed of Interger method Integer
, interpreted as Integer.method(Integer)
, that is, the operator is a method of its predecessor class, and the operand is the method argument. Remember that the use of relatives is optional?! This is an example of that!
Although this may seem obvious, the way you use this language feature may not be so obvious. To illustrate this need, consider the need to compare two distinct objects to see if they are considered equal.
For example, an integration system that has two data structures for the same product in the real world.
product_from_app_A = Struct.new(:id, :name, :serial).new('123', 'Gift Card (christmas edition)', 'ABC123')
product_from_app_B = Struct.new(:reference_code, :title, :key).new('123', 'Gift Card - CE598/BR','ABC123')
product_from_app_A == product_from_app_B # => false
# Overwrite the == method to compare the objects using your custom logic
class Struct
def ==(other)
self.id == other.reference_code && self.serial == other.key
end
end
product_from_app_A == product_from_app_B # => true
Note, that in the example above, we overwrite the ==
method of the Struct
class to compare the objects using our custom logic. This is a very common practice in Ruby, and this is so flexible that allows you to overwrite methods of any class.
Of course, this example is very simple, but the idea is to abstract the concept in appropriate situations. And to help with these validations, in addition to the traditional ways of comparing information, Ruby implements an operator known as the ‘Spaceship operator‘
for larger., which is used to compare two values and return an integer value, with
-1 for smaller,
0 for equal and
1
1 <=> 2 # => -1
2 <=> 2 # => 0
3 <=> 2 # => 1
Still on the subject of comparators, a method that is easily confused with an operator is <<
, and this happens because we normally find this method written in an infixed form, that is, between two operands, and because we associate the <
character with a comparison.
# BAD
'foo'.<<('bar') # => "foobar"
# NOT BAD
'foo'.concat('bar') # => "foobar"
# GOOD
'foo' << 'bar' # => "foobar"
# BAD
[1,2,3].<<(4) # => [1, 2, 3, 4]
# NOT BAD
[1,2,3].push(4) # => [1, 2, 3, 4]
# GOOD
[1,2,3] << 4 # => [1, 2, 3, 4]
The method <<
is used to concatenate strings or to add an element to an array, but as we have seen, it can also have its own implementation to join objects in a custom way. Tip! The <<
method handles only one argument, while .concat
or push
can handle multiple arguments.
Another method that can be very useful for extracting string excerpts is the method slice
, (for Strings
) which can also be invoked with the []
operator. It is worth remembering that the argument of the slice method can be two integers, or an interval, known as Range, which we will see later in this post.
# NOT BAD
'hello'.slice(0, 2) # => "he"
# GOOD
'hello'[0, 2] # => "he"
'hello'[0...2] # => "he"
In addition to these widely known methods, Ruby classes also have conversion methods, popularly known for their function of parsing (changing) the data format, but used in Ruby in a simpler and more intuitive way.
1.to_f #=> 1.0
1.0.to_i #=> 1
h = { foo: 'bar'}
h.to_a #=> [[:foo, "bar"]]
h.to_json #=> "{\"foo\":\"bar\"}"
h.to_s #=> "{:foo=>\"bar\"}"
Of course, these methods are relative to each class type and are only applied with type coercion, that is, the conversion from one type to another in a valid way. It is also worth noting that the syntax in this case, for instance, methods, requires the use of .
to invoke the method.
# BAD
1 to_f # => SyntaxError (SyntaxError)
# GOOD
1.to_f # => 1.0
Methods
A common confusion among beginner developers, especially those who start their journey directly with Ruby on Rails, is knowing the difference between language methods and framework methods and when to apply each of them.
This topic could be the subject of another post, but to summarize, I recommend that you explore the Ruby documentation, where you can find all the information you need to understand the language and its native methods. And to make you curious to go do this, here is a list of methods that can save you time if you know they exist haha!
- Array#cicle
- Array#sample
- Array#take
- Enumerable#tally
- Enumerable#partition
- Hash#transform_keys
- Kernel#tap
- String#squeeze
- Range#step
Methods like time_ago_in_words
, pluralize
, number_to_currency
, and presence
is often mistaken as Ruby methods, but in fact, they are Rails framework methods.
Logical Operators
Logical operators are commonly related to comparison operations and return boolean values, i.e. true
or false
. Let's look at some examples.
true && true # => true
true && false # => false
true and false #> false
true || false # => true
true or false # => true
!(true) # => false
not(true) # => false
Note that in the code above we perform logical comparisons using &&
and and
, and ||
and or
, as well as !
and not
. Although these methods perform the same logical comparison, the way the language interprets each of them can be different, and therefore, it is essential that you know when to use each of them.
Precedence
When we use logical operators such as &&
, and
, ||
, and or
in Ruby, the precedence between them can directly impact the result of the expressions. Operators such as &&
and ||
have higher precedence compared to and
and or
. This means that operations with &&
and ||
are evaluated before and
and or
in the same expression, unless they are explicitly grouped by parentheses.
result = false && true or true
# equivalent to:
# result = (false && true) or true
# => true
result = false and true or true
# equivalent to:
# (result = false) and (true or true)
# => false
Understanding precedence is essential to avoid unexpected behavior when combining these logical operators in a single expression.
Boolean Operators
As in other languages, some behaviors are common about application objects, in other words, it is possible to say that a boolean value false
is considered 'falsy', and a value true
is considered 'truthy'. These operators can be useful to check the presence and state of objects or their attributes. It is worth remembering that in Ruby, only false
and nil
is considered 'falsy'. Let's look at the following example:
false # => falsy
nil # => falsy
true # => truthy
0 # => truthy
'' # => truthy
[] # => truthy
{} # => truthy
Knowing whether an object exists, whether it is blank, or whether it has a null value, can be done in a simple and direct way, without the need to use conditional operators to validate the value of the desired attribute.
text = ''
puts 'text is truthy' if text # => text is truthy
puts 'text is falsy' if !text # => nil
In the example above, the variable text is considered truthy
, beucase it is defined, and it has a value, even if the value is an empty string.
Decision Structures
In Ruby, decision structures allow you to conditionally control the flow of code execution.
if, else, elsif
For a complete decision structure, we use if
, else
, and elsif
, as shown in the example below, but it is not always necessary to use this entire structure.
def check_number(number)
if number > 5
puts 'number is greater than 5'
elsif number < 5
puts 'number is less than 5'
else
puts 'number is equal to 5'
end
end
check_number(3) # => number is less than 5
check_number(5) # => number is equal to 5
check_number(7) # => number is greater than 5
Ternary if
Another way to write the if
decision structure is using the ternary operator ?
. It is useful for expressing simple decisions in a single line. Its structure is represented by condition ? true : false
. Let's look at the following example:
text = 'short'
# BAD
def validate_text_length(text)
if text.length > 10
puts 'text is too long'
else
puts 'text is ok'
end
end
# GOOD
def some(text)
puts text.length > 10 ? 'text is too long'' : 'text is ok'
end
We can often simplify the code by using just if
, especially when we don't have a condition for else
, so it is recommended to use the inline structure, as it is more readable and concise.
text = 'short'
# BAD
if text.length > 10
puts 'text is too long'
end
# GOOD
puts 'text is too long' if text.length > 10
# GOOD
puts 'text is too long' unless text.length <= 10
The unless
operator is the opposite of the if
. Acting as a negated if
, it executes the code if the condition is false.
puts 'something' if true # => something
puts 'something' unless true # => nil
# equivalent to:
# puts 'something' if !true
# => false
As a developer, you will find that this syntax can be very readable and easy to understand, especially to reduce the number of nested if/elses, since you can perform all the validations before executing the desired code.
# BAD
def validate_text_length(text)
if text.length > 10
puts 'text is too long'
else
puts 'text is ok'
end
end
# GOOD
def validate_text_length(text)
return puts 'text is too long' if text.length > 10
puts 'text is ok'
end
This example above is not necessarily language-related, but rather an example of how you can simplify your code, and make it more readable and cohesive with Ruby's features.
And a final type of decision structure is the well-known case/when
, which is used to compare a variable with several values(branches).
def check_number(number)
case number
when 10
puts 'Ten'
when 20
puts 'Twenty'
else
puts 'Another Value'
end
end
But it is not always possible to know the exact value to which the variable will be compared, and for this reason, we can write the case/when
with more complex comparisons, as in the following example:
def check_number(number)
case
when number < 0
puts 'The number is negative'
when (1..5).include?(number)
puts 'The number is between 1 and 5'
when number >= 6 && number <= 10
puts 'The number is between 6 and 10'
else
puts 'The number is greater than 10'
end
end
check_number(-1) # => The number is negative
check_number(3) # => The number is between 1 and 5
check_number(7) # => The number is between 6 and 10
check_number(11) # => The number is greater than 10
However, this type of operator is not usually used that often, but it can be very useful in scenarios where you need to perform a certain action based on a parameter that can be a limited set of options. It is worth remembering that in Ruby, the traditional default
statement is replaced by else
.
Repetition Structures
Also known as 'Iterators', the Ruby language implements several ways of iterating over a set of data, from the structured way, a traditional for
, to a more flexible way, such as each
.
# For Loop
for i in 1..3
puts i
end
# => 1
# => 2
# => 3
# Collect Iterator
colors = ['red', 'green', 'blue']
colors.each do |color|
puts color
end
# => red
# => green
# => blue
# example with index
colors.each_with_index do |color, index|
puts "#{index + 1}: #{color}"
end
# => 1: red
# => 2: green
# => 3: blue
However, the number of iterations on a data set can sometimes be relative, running for a period of time, or for 'x' repetitions, or even until a condition is met. For this, we have the iterators while
, until
, times
, step
, each_line
, upto
, downto
, and loop
, which work in a very similar way.
# While / Unless
i = 0
while i < 3
i += 1
puts i
end
# => 1
# => 2
Before we look at the next examples, when it comes to iterators, Ruby also offers us the reserved word next
, which allows you to skip the current iteration and go to the next one, and break
, which allows you to exit the loop.
i = 0
while i < 5
i += 1
puts i
next if i == 2
puts 'first continue'
break if i == 3
puts 'second continue'
end
# => 1
# => first continue
# => second continue
# => 2
# => 3
Now, going back to the other examples we have:
# Step Iterator
(1..10).step(2) do |n|
puts n
end
# => 1
# => 3
# => 5
# => 7
# => 9
# Times Iterator
3.times do |n|
puts 'Hello World'
end
# => 'Hello Word'
# => 'Hello Word'
# => 'Hello Word'
# EachLine Iterator
text = "Hello, world!\nWelcome to Ruby.\nEnjoy coding!"
text.each_line do |line|
puts "Line: #{line}"
end
# => Line: Hello, world!
# => Line: Welcome to Ruby.
# => Line: Enjoy coding!
# UpTo Iterator
1.upto(5) do |i|
puts "Counting up: #{i}"
end
# => Counting up: 1
# => Counting up: 2
# => Counting up: 3
# => Counting up: 4
# => Counting up: 5
# DownTo Iterator
5.downto(1) do |i|
puts "Counting down: #{i}"
end
# => Counting down: 5
# => Counting down: 4
# => Counting down: 3
# => Counting down: 2
# => Counting down: 1
# Loop Iterator
loop do
puts Time.now.sec
break if Time.now.sec >= 59
end
Ranges
In Ruby, Ranges are used to represent a sequence of values, such as numbers or letters. There are two main types of ranges, and the difference between them is whether the final value is included or excluded:
# Inclusive range
(1..3).to_a # => [1,2,3]
# Exclusive range
(1...3).to_a # => [1,2]
An important thing about range is that in certain situations it is necessary to encapsulate the range in parentheses to evaluate its values, or assign the range to a variable.
# BAD
1..3.to_a # => NoMethodError
# GOOD
range = 1..3
range.to_a # => [1,2,3]
# GOOD
(1..3).to_a # => [1,2,3]
Prefixes / Sulfixes
It is common to find Ruby codes using prefixes and suffixes, and at first glance, they may seem quite confusing, but with an understanding of what each one represents and practice, it is easy to get used to it.
Instance Variable – @
The @
prefix is used to define instance variables within a class, that is, these variables are accessible in their context during code execution.
class Person
def initialize(name)
@name = name # => set the instance variable
end
def name
@name # => access the instance variable out of the scope where it was defined
end
end
Person.new("Carlos")
_.name # => "Carlos"
Note that for this example, we used _
to access the recent object created in memory. Using _
is a lesser known handy method used to access the last object in memory, which can be very useful in certain situations, especially when you need to access the return of a method that you don't want to assign to a variable.
Class Variable – @@
The prefix @@
or $
are used to define class variables, also known as global variables, which are shared among all instances of a class.
But don't worry, in most cases using only class variables may not be a good option because they can be easily overwritten and cause unwanted side effects. However, depending on your project, this can be useful. Let's see an example below of how to use this type of variable.
class Car
@@total_cars_manufactured = 0 # => class variable
def initialize
@@total_cars_manufactured += 1 # => increment the class variable
end
end
Car.new # => #<Car:0x00007f85a6645c20>
Car.new # => #<Car:0x00007f85a656db68>
Car::total_cars_manufactured # => 2 (accessing the variable by a class variable)
Note that in this case, we access the class variable @@total_cars_manufactured
using the ::
operator, which is used to access both class variables and class constants.
And since we are talking about the ::
operator, you may come across code similar to the following example at some point in your career:
FOOBAR = "Global FOOBAR"
module ParentModule
FOOBAR = "ParentModule FOOBAR"
module ChildModule
FOOBAR = "ChildModule FOOBAR"
def self.show_constants
puts FOOBAR
puts ParentModule::FOOBAR
puts ::FOOBAR
end
end
end
ParentModule::ChildModule.show_constants
# => ChildModule FOOBAR
# => ParentModule FOOBAR
# => Global FOOBAR
When the ::
operator is used without a prefix, it is interpreted as a scope resolution being used to access information in the global scope of the application. Depending on the application, you may find methods like ::SomeService.call
and SomeService.call
, representing the same service, in different places in the application (usually when service name ambiguities occur in different contexts). This can also easily happen if you use your lib, where the files of this lib are not necessarily in the specific directory of the application, that is, to access the file lib/some_service.rb
you can use ::SomeService
and use SomeService
to access the file app/services/some_service.rb
.
Bang Methods – !
The suffix – not to be confused with the prefix – !
is used to indicate methods that modify the original object or that can generate side effects such as irreversible changes to the application data.
In projects that use Ruby as a language, it is common to have "non-destructive" versions of the bang methods, that is, without the !
h = { a: 1 }
# NON-DESTRUCTIVE - perform the logic without modifying the original object
h.merge(b: 2) # => {:a=>1, :b=>2}
h # => {:a=>1}
# DESTRUCTIVE - modify the original object
h.merge!(b: 2) # => {:a=>1, :b=>2}
h # => {:a=>1, :b=>2}
Interrogation Methods – ?
The suffix ?
tends to indicate that the method returns a boolean value (true
| false
). It is mostly used to check the conditions or states of objects.
''.blank? # => true
'foo'.blank? # => false
[].empty? # => true
[1,2,3].empty? # => false
0.zero? # => true
1.zero? # => false
=
– Assignment Methods
And the last suffix =
, is used to define assignment methods. These methods allow you to set or update the value of an attribute of an object in a similar way to an instance variable, but with a layer of abstraction.
class Person
attr_reader :age
attr_accessor :name
def initialize(name:)
@name = name
end
def age=(age)
@age = age
end
end
person = Person.new(name: 'Carlos')
person.age # => nil
person.age= 24
person.age # => 24
person.name # => Carlos
person.name= 'Ana' # => Ana - The attr_accessor method creates the getter and setter method for name
person.name # => Ana
It is worth noting that these assignment methods can be easily replaced by the attr_accessor method that does the job of defining the reading and writing methods for an attribute of a class, as is the case for name
.
Note that the attr_reader
method is used to define only the reading method for the age
attribute, while the attr_accessor
method defines both the reading and writing methods for the name
attribute. In addition to these, we also have the attr_writer
method that defines only the writing method for an attribute.
Safe navigation
In Ruby, accessing a method on an object that is nil
results in a NoMethodError error.
user = nil
user.profile.name # => NoMethodError (undefined method `profile' for nil:NilClass))
To avoid this, we often end up writing explicit checks to ensure that the object is not null before calling the method, like the following code snippet:
if user && user.profile
user.profile.name
end
To solve this trivial problem, it is possible to use the &.
suffix, known as 'Safe Navigation', which was introduced in version 2.3 of Ruby. Using this method allows you to access methods or attributes of objects safely, avoiding errors when an object is nil.
The operator automatically checks whether the object that invoked it is not nil
. If it is, it returns nil
instead of throwing an exception. Otherwise, it calls the method or accesses the attribute normally.
user = nil
user.profile.name # => NoMethodError (undefined method `profile' for nil:NilClass))
user&.profile&.name # => nil
Safe navigation on complex data:
In the same way that safe navigation is used to access object values, there may be situations where you need to perform safe navigation in complex data, such as nested hashes. For this example, let's consider the following hash:
data = { user: { address: { city: "São Paulo" } } }
If you try to access a key that doesn't exist, like state, Ruby will throw an error without safe navigation:
data[:user][:address][:state] # => NoMethodError (undefined method `[]' for nil (NoMethodError))
Therefore, to have the same behavior as safe navigation, we can use the method .dig
which was also introduced in version 2.3 of Ruby.
data.dig(:user, :address, :state) # => nil
Safe navigation is very useful when you are consuming external APIs, where certain attributes may be missing. A very common scenario for this is when the endpoint consumes information from a non-relational database, where the object's attributes may or may not exist.. In this way, using safe navigation resources helps to avoid possible unwanted application crashes and drastically reduces the amount of if
statements throughout your code.
Block and Proc
Blocks and Procs are two ways of dealing with reusable code snippets in Ruby. These language features allow you to pass and execute code in a flexible way, but they have subtle differences. Well, before we continue, let me tell you something: if you've gotten this far, you're already using Blocks and Procs, even if you don't know it!
Block
A Block is a piece of code that can be passed to a method and executed within that method. In Ruby, blocks are delimited by do...end
or curly braces {}
. The most common is to use them in higher-order methods like each
, map
, select
, etc.
# do...end
[1,2,3].each do |n|
puts n
end
# {}
[1,2,3].each { |n| puts n }
Quite similar to the examples we've seen so far, isn't it? Consider that it is possible to define methods that can receive a Block, but this block can only be passed once per method call, and when this occurs the block is interpreted as yield
. Blocks are used quite frequently to iterate over collections and apply specific logic as we saw previously.
def greet
puts "before the block"
puts yield
puts "after the block"
end
# do...end
greet do
"hello world!"
end
# {}
greet { "hello world!" }
# => before the block"
# => hello world!
# => after the block
Another feature is that we can validate whether a Proc was passed to a method using the block_given?
method. It is also possible to pass arguments to a block, see the following example:
def show_numbers
[1, 2, 3].each do |n|
yield(n) if block_given?
end
end
show_numbers # => nil (no block given)
show_numbers do |num|
puts "Number: #{num}"
end
show_numbers { |num| puts "Number: #{num}" }
# => Number: 1
# => Number: 2
# => Number: 3
Procs
A Proc – short for "procedure" – is an object that encapsulates a block of code, allowing it to be stored in a variable, passed as an argument to methods and executed when necessary. It is worth mentioning that to execute the code of a Proc we use the .call
method, and not yield
as in a Block.
my_proc = Proc.new { puts "hello from Proc!" }
my_proc.call # => hello from Proc!
Just like a block, a Proc can receive arguments, see the following example:
my_proc = Proc.new { |name| puts "hello, #{name}!" }
my_proc.call("Carlos") # => hello, Carlos!
my_proc.call("Ana") # => hello, Ana!
To summarize, the main difference between Proc and Block is that a Proc can be reused and passed multiple times, while a Block is directly associated with a single execution.
A Block is not an object, but a piece of code that can be implicitly passed to a method.
A Proc is an object and can be stored in variables and explicitly passed to methods.
Methods can receive multiple Procs, but only one Block.
Assignments and Decomposition
If you've ever had the opportunity to explore a medium or large Ruby project, you've probably come across expressions known as multiple assignment and decomposition.
Multiple Assignment
Multiple Assignment is a technique that allows you to assign multiple values to multiple variables at once. In Ruby, we can use this technique to assign values to variables in a concise and readable way.
x, y, z = 1, "Hello", true # => [1, "Hello", true]
x # => 1
y # => "Hello"
z # => true
# If there are more variables than values, the extra variables will be assigned nil
a, b, c = 1, 2 # => [1, 2] - c is not printed because it is nil
a # => 1
b # => 2
c # => nil
# If there are more values than variables, the extra values will be ignored:
a, b, c = 1, 2, 3, 4 # => [1, 2, 3, 4]
a # => 1
b # => 2
c # => 3
source code: Multiple Assignment and Decomposition | Exercism
Decomposition
Decomposition is used to deal with method arguments and hashes. Similar to when we did a multiple assignment, we can decompose an array or a hash into individual variables. In the following example, we will use the suffixes *
and **
which are used to decompose an Array
and a Hash
, respectively.
# Decomposition with Arrays
def my_method(a, b, c)
p c
p b
p a
end
numbers = [1, 2, 3]
my_method(*numbers) # => *numbers decomposes the array into individual arguments
# => 3
# => 2
# => 1
source code: Multiple Assignment and Decomposition | Exercism
Note that we passed an array as an argument, but the method expects to receive three arguments. Therefore, using *
unpacks the array and passes each element as an individual argument to the method. The same happens to decompose a hash, but in this case, we use **
, but this time, the hash must have keys that correspond to the method arguments.
# Decomposition with Keyword Arguments
def my_method(a:, b:, c:)
puts a
puts b
puts c
end
numbers = {a: 1, b: 2, c: 3}
my_method(**numbers) # => **numbers decomposes the hash into keyword arguments
# => 1
# => 2
# => 3
source code: Multiple Assignment and Decomposition | Exercism
But Ruby also allows you to do things a little differently! And when I say differently, I mean making the number of arguments of a method dynamic with the language features. See the following example:
def my_method(**kwargs)
puts kwargs[:foo] # => foo
puts kwargs[:bar] # => bar
puts kwargs[:baz] # => nil (#1), baz (#2)
end
my_method(foo: 'foo', bar: 'bar')
my_method(foo: 'foo', bar: 'bar', baz: 'baz')
The same can also be done as seen in the following example:
def my_method(*args)
puts args[0] # => foo
puts args[1] # => bar
puts args[2] # => nil (#1), baz (#2)
end
my_method('foo', 'bar')
my_method('foo', 'bar', 'baz')
This type of method is very useful when you need to deal with dynamic arguments. This way, when your method receives a hash of options like options:
, you can use **options
and, based on boolean operators, execute the desired logic in a more intuitive and readable way.
Did you like this session!? Then I highly recommend reading the multiple assignment and decomposition track. This article explores these concepts in more depth, with a lot of explanation about 'Deep decomposing' and how they can be applied in everyday situations. I also recommend taking the challenge so you can practice and improve your skills.
Syntax shorteners
As a good practice in the Ruby language that seeks to maintain the most natural syntax possible, there are some ways to shorten the code writing, making it more readable and concise. Let's see some examples below:
While
Let's start with a traditional example of how to iterate over a list of numbers and transform them into strings.
a, result, i = [1, 2, 3], [], 0
while i < a.size do
result << a[i].to_s
i = i + 1
end
Here we have a while loop, a simple and explicit approach where we iterate over each item in the list manually, checking the length of the list and incrementing the variable i
. This code is functional, but in Ruby, we can do better with fewer lines and in a more readable way.
For
result = []
for n in [1,2,3] do
result << n.to_s
end
With for, we iterate directly over the elements of the list, without needing an index variable. Still, the for loop in Ruby is not the most idiomatic way to iterate over collections.
Block
Ruby offers higher-order methods that allow you to manipulate collections more concisely. The map method is a perfect example. It applies a block of code to each element of the collection and returns a new list with the results.
[1,2,3].map{ |n| n.to_s }
Here, we pass a block to the map method, which transforms each number into a string. This code is already leaner and more expressive, eliminating the need for an explicit result array.
Symbol#to_proc
Now we come to the most idiomatic Ruby way to accomplish this task. Instead of passing a full block, we can use the &:
operator to transform the to_s
method into a Proc
, which will behave similarly to an anonymous function and in this case, will call the .to_s
method on each element of the list.
[1,2,3].map(&:to_s)
This line does exactly the same thing as the previous examples, but with the maximum conciseness and elegance that Ruby offers.
Still on the topic of shortcuts, it is possible to use some syntax shortcuts to make the code more readable when defining lists, such as %w
and %i
for lists of string
and symbol
, respectively.
# BAD
list_of_str = ['admin', 'editor', 'viewer']
list_of_sym = [:admin, :editor, :viewer]
# GOOD
list_of_str = %w[admin editor viewer]
list_of_sym = %i[admin editor viewer]
Conclusion
Well, if you already know all these features of the Ruby language, congratulations! You are on the right track to becoming a successful Ruby developer. If you are new to the language, I hope this article has helped you better understand the basic and advanced concepts of the language.
If you are interested in learning more, I recommend that you continue practicing and exploring the features of the language, after all, the best way to learn is to get your hands dirty and try it out for yourself! One last tip I can give is that if you are coming from another language, it is good to make comparisons between the two. However, before you start comparing, spending some time to understand the philosophy of the Ruby language can be very useful and help you learn more efficiently.
We want to work with you. Check out our "What We Do" section!