Ruby for the Curious image cover

Ruby for the Curious: A Hands-On Guide to Getting Started

Ruby Basics to Pro Tips: Learn by Doing!

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, which is used to compare two values and return an integer value, with -1 for smaller, 0 for equal and 1 for larger.

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!

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!

Lucas Geron

 I'm always looking for ways to learn and grow. I enjoy contributing to projects that make a real difference. Clear communication and respect are important to me – it's how we achieve our goals together!     |     Back-end Developer at CodeMiner42

View all posts by Lucas Geron →