A comprehensive introduction to Ruby

Another tutorial for learning Ruby? Absolutely not. This guide is different from the typical tutorials you'll find online.

As someone new to Ruby, you've likely already completed a few tutorials. Instead of providing yet another tutorial, this guide aims to offer unique insights and valuable resources to help you further your understanding of Ruby.

🤔 But I've never completed any tutorial in Ruby

Even if you haven't completed any tutorials, this guide is designed to help you from the very beginning. By focusing on understanding the fundamentals, you'll be able to practice with greater confidence and build a solid foundation in Ruby.


Index

In this article, we'll cover the following topics:


🖖 First things first

Ruby is a versatile general-purpose language that can be used across various domains, from web development to IoT.

However, it has gained significant adoption in the web development industry, particularly in the startup scene since the mid-2000s.

It can be downloaded in several ways, including using a Docker image for those who prefer using Docker.


💎 How is Ruby implemented

If you downloaded Ruby from the official website or used the default Docker image, you likely have the standard Ruby implementation: CRuby, which is primarily written in C.

Although there are several implementations of Ruby, such as JRuby and Rubinius, CRuby is the most commonly used among major companies such as Shopify and Github.


⌛ A time travel to the Ruby's history

Ruby was created in the early 1990s by Yukiriro Matsumoto, or "Matz", a programmer and computer scientist in Japan.

🟢 1993, where it all started

Matz started the Ruby implementation in 1993, drawing inspiration from existing languages like Perl and Smalltalk. His implementation was called CRuby, or "MRI" (Matz Ruby Interpreter).

👉 Ruby is interpreted CRuby is an interpreter, meaning that the source code is not compiled into optimized code; instead, it is interpreted line-by-line and translated to low-level instructions during execution.

CRuby before YARV

🟢 1995-2000, big in Japan

Ruby was first released publicly in 1995, and until 1999, several features and improvements were added to the standard library. However, its adoption remained limited to Japan.

In 2000, Dave Thomas and Andy Hunt published the first English-book on Ruby, "Programming Ruby" (a.k.a The Pickaxe Book), which played a crucial role in popularizing Ruby outside of Japan.

🟢 2004, Ruby on Rails

Ruby on Rails, a web application framework built with Ruby, was released by David Heinemeier Hansson (a.k.a DHH). This framework played a significant role in Ruby's widespread adoption in the web development community.

🟢 2007, meet YARV

CRuby was known to have various performance problems due to its interpretation process, lacking a compilation process such as JIT (Just-in-time compilation).

During the development of version 1.9, the Ruby core team decided to refactor a significant portion of the CRuby source code. This resulted in the creation of the Ruby VM, which was integrated into the CRuby interpreter.

The new VM, called YARV (Yet Another Ruby VM, duhh :P), takes the parsed tree (AST) and generates a bytecode instead of low-level instructions, enabling optimizations to be made. This bytecode is then translated into optimized low-level instructions, resulting in improved performance.

YARV

Later, in the version 2.6, a new JIT was introduced, called MJIT. This uses a separate thread to perform optimizations on a portion of Ruby code, which are then joined during the execution process.

YARV + MJIT

🟢 2021, the Ruby 3.0

After years of continuous improvement, Ruby 3.0 was realeased in 2021, introducing new features to address the lack of concurrency, parallelism, and async I/O.

Ractors were introduced to enable parallelism on CPU, while Fibers brought a new scheduler interface that enables cooperative scheduling. When used with non-blocking I/O, this allows for faster Ruby applications that are heavily I/O bound.

🟢 YJIT, a new hope

The Ruby core team is currently developing a new version of Ruby that includes a faster JIT compiler called YJIT. This new JIT is expected to be faster than the current MJIT, providing significant performance improvements for Ruby applications.


💎 Ruby key features

Ruby is a high-level programming language that uses an interpreter to translate Ruby code into low-level code.

Some of the key features of Ruby include:

👉 Dynamic types Ruby is a dynamically typed language, which means that variable types are determined at runtime.

👉 Object oriented Not only Ruby supports OOP, everything in Ruby is an object, including primitive data types like numbers and strings. By using dynamic dispatch, Ruby combines data and behaviour for almost everything in a Ruby program.

👉 Mixins Ruby uses mixins instead of multiple inheritance, which allows for modular and reusable code. Mixins are achieved through the use of modules, which can be included in classes to provide additional functionality.

👉 Garbage collected Ruby brings automatic memory management, taking care of allocating and freeing memory of objects as needed. This process is called Garbage Collection, or GC.

👉 Metaprogramming Ruby allows for metaprogramming, which means that you can write code that generates or modifies other code at runtime. This feature enables expressive and flexible programming techniques like domain-specific languages (DSLs).

👉 Functional programming Although Ruby is not a purely functional language, it supports functional programming concepts like high-order functions and immutable data structures. It also includes functional constructs like blocks, procs and lambdas, which enable the creation of expressive and concise code.


💎 Hello, world

Every programming language deserves a "Hello, world" example, and Ruby is no exception. Here's the simplest "Hello, world" program you can write in Ruby:

$ ruby -e 'print "Hello, world\n"'
Hello, world

Alternatively, Ruby provides a method called puts that automatically appends a \n character to the given text:

$ ruby -e 'puts "Hello, world"'
Hello, world

💎 IRB

Ruby's interactive shell, known as IRB (Interactive Ruby), provides a convenient way to experiment with Ruby code and test small code snippets without needing to write and execute a full program.

$ irb
irb(main):001:0>

Now, in IRB:

1 + 1
=> 2

💎 Data Types

Ruby includes data types commonly used in mainstream technologies:

+------------+------------------------------+
| Data Type  | Examples                     |
+------------+------------------------------+
| Integer    | 42, -10, 0                   |
| Float      | 3.14, -1.23e-4               |
| Boolean    | true, false                  |
| String     | "Hello, world!", 'Ruby'      |
| Array      | [1, 2, 3],                   |
|            | ['apple', 'banana', 'cherry']|
| Hash       | {name: 'Alice', age: 25}     |
| Symbol     | :name, :age, :status         |
| Nil        | nil                          |
+------------+------------------------------+

👉 Ruby is dynamically typed As mentioned earlier, Ruby is a dynamically-typed language, meaning that variable types are determined at runtime.

name = "Leandro"
age = 18

It's worth noting that Ruby doesn't require variable type specifications.

Instead, the interpreter dynamically infers the value's data type at runtime, allowing programs to execute successfully without explicit type declarations.

We can confirm this behavior by calling the class method on variables:

name.class
=> String

age.class
=> Integer

But how it's possible to call methods from primitive data types in Ruby?


💎 Everything in Ruby is an object

Since everything in Ruby is an object, even primitive data types such as strings and integers can have instance methods called on them.

This makes Ruby a highly object-oriented language, where every piece of data is represented as an object with its own state and behavior.

name.object_id
=> 80

age.object_id
=> 37

1.class
=> Integer

1.object_id
=> 3

Before we dive deeper into object-oriented programming in Ruby, it's important to understand what an object is and how it works in the context of the language.

🔵 OOP

Alan Kay, one of the pioneers of object-oriented programming (OOP), defined an object as a unit of software that encapsulates data and behavior.

This definition means that an object combines data (state) and the code that manipulates that data (behavior), resulting in a more modular and organized approach to programming.

🔵 OOP in Ruby

To create objects in Ruby, we need to define a template that can generate multiple objects. In Ruby, this template is defined using the class keyword:

class Account
end

Objects in Ruby, also called instances, can be created by calling the method new in the class:

Account.new # an instance of the Account class
Account.new # another instance
Account.new # yet another instance
...

We can confirm that an instance object represents the Account class and holds an object ID by calling the instance methods class and object_id:

account = Account.new

account.class
=> Account

account.object_id
=> 60

👉 Classes are objects too Remember, everything in Ruby is an object. Classes are no different.

Classes are just instances created from the class Class:

Account.class
=> Class

Account.object_id
=> 80

classes are objects

👉 Also, Integers are objects We can represent the integers as instances from the Integer class, since Integer is an instance of the Class class:

Integers are objects

👉 Single inheritance

Ruby uses single inheritance, which means that every class inherits from a single superclass. It's worth noting that every class in Ruby will inherit from the BasicObject parent class in some way.

We can verify this behavior in Ruby by examining class hierarchies:

Account.new.class     # Account
Account.class         # Class
Account.superclass    # Object

1.class               # Integer
Integer.class         # Class
Integer.superclass    # Numeric
Numeric.superclass    # Object

Class.superclass      # Module
Module.superclass     # Object

Object.superclass      # BasicObject
BasicObject.superclass # nil

Based on the example above, here's a portion of the Ruby class hierarchy:

class hierarchy

🔵 Modeling OOP in Ruby

In Ruby, everything is an object, which means that instances can hold state (attributes) and behavior (methods).

In our current implementation, the Account class has no visible state or behavior, as it inherits all of its attributes and methods from the Object class. However, let's say we want every Account instance to have a starting balance of zero.

Let's see the solution.

👉 The initialize method Ruby provides a special method called initialize, which is automatically called every time a new instance is created (i.e., when Account.new is called).

By default, initialize is empty, so there's no need to define it. However, since we want to modify the initialization of the object, we should override the initialize method:

class Account
  def initialize
    @amount = 0
  end
end

leandro = Account.new
john = Account.new

You might be wondering about the @ character that appears before the variable name in our example. In Ruby, variables inside methods are typically treated as local variables by default:

def some_method
  name = "Leandro"
end

It's important to note that local variables within methods are discarded by the interpreter when the method is finished.

To store variables that should persist during the lifetime of an object (i.e., its state), we should use instance variables instead. In Ruby, instance variables are denoted by the @ symbol.

Here's a visual representation of instance variables:

initialize with instance variable

👉 Objects in Ruby are encapsulated In Ruby, an instance's state is private by default, which means that we cannot directly access an object's state from outside the object. Instead, we can use instance methods to retrieve or modify the object's state.

To demonstrate this, we can create an instance method called balance, which will return the current balance of the account object:

class Account
  def initialize
    @balance = 0
  end

  def balance
    @balance 
  end
end

With the balance method defined, we can now retrieve the current balance of our account instance:

leandro = Account.new

leandro.balance 
=> 0

It's true that state without manipulation is often useless. To make our Account class more practical, we can define another instance method that will modify the balance:

class Account
  def initialize
    @balance = 0
  end

  def balance
    @balance
  end

  def set_balance(balance)
    @balance = balance
  end
end

leandro = Account.new
leandro.set_balance(10)

leandro.balance
=> 10

In the set_balance method, it's important to understand the difference between balance and @balance:

👉 Syntactic sugar all the way Now, suppose we want to modify the balance using a setter method. In Ruby, we can define setter methods by appending an = sign to the method name, like so: balance=(balance):

class Account
  ...
  def balance
    @balance 
  end

  def balance=(balance)
    @balance = balance
  end
end

leandro = Account.new
leandro.balance=(10)

leandro.balance
=> 10

Ruby is a language that values simplicity and readability, and it makes use of syntactic sugar to achieve this. One example of this is the ability to omit parentheses when calling methods:

leandro.balance=(10) 

# becomes

leandro.balance= 10

In fact, Ruby goes even further in its quest for readability and simplicity. When calling a method with a suffix of =, we can omit the parentheses and instead add a blank space before the = sign:

leandro.balance= 10

# becomes

leandro.balance = 10

Pretty cool, isn't it? With this syntax, we can make method calls that look like variable assignments!

class Account
  def initialize
    @balance = 0
  end

  def balance
    @balance
  end

  def balance=(balance)
    @balance = balance
  end
end

👉 attr_reader/writer/accessor One of the great things about Ruby is that it provides built-in class methods like attr_reader that can help make our code more concise and readable.

In particular, attr_reader works like a getter method for instance variables, allowing us to access their values from outside the class:

def balance
  @balance  
end

# becomes

attr_reader :balance

In addition to attr_reader, Ruby also provides attr_writer, which works like a setter method for instance variables:

def balance=(balance)
  @balance 
end

# becomes

attr_writer :balance

Together, these two methods can help make our code more concise and easier to read, enabling us to quickly define and manipulate instance variables with ease:

class Account
  attr_reader :balance
  attr_writer :balance
  
  def initialize
    @balance = 0
  end
end

To further simplify the code, we can use attr_accessor when we have both a attr_reader and attr_writer method for the same instance variable:

class Account
  attr_accessor :balance
  
  def initialize
    @balance = 0
  end
end

leandro = Account.new
leandro.balance = 10

leandro.balance
=> 10

Ruby truly does make programming a joy!


Wrapping Up

In this post, we provided a comprehensive introduction to the Ruby programming language.

We covered a wide range of topics, from the implementation of Ruby and its history to its key features and data types.

Finally, we explored the key concept that everything in Ruby is an object, giving you a solid foundation for understanding the language's core principles.

In future posts, we will continue to explore the key features of Ruby, including scopes, the self context, blocks, lambdas, and much more.

By delving deeper into these topics, I hope to provide you with a comprehensive understanding of Ruby that will enable you to write powerful, effective code with ease.

Stay tuned!


References

Ruby official websiteWikipedia - Ruby programming languageWikipedia - InterpreterMJIT: a method based JIT compiler for RubyRuby implementationsYJIT - building a new JIT compiler for CRubyYJIT - a basic block versioning JIT compiler for CRubyRuby 3.0 release notes


This post was written with the assistance of ChatGPT, which helped to compile information and identify grammar errors and inconsistencies.