January 28, 2025
Paweł Dąbrowski
Founder / Software Engineer
As Ruby developers, we live in a world full of readable DSL expressions, self-explaining code, and functions that we can almost read like English sentences. Have you ever wondered how someone built methods we often see in libraries and the Ruby on Rails framework?
I decided to investigate the source code for an explanation and to learn how to create such constructions myself. This article collects popular Ruby methods and provides a simple explanation of how to recreate them from scratch.
I bet methods #blank?
and #present?
are some of the most popular Ruby on Rails methods in a Rails application. They are great because they are very flexible and available on all objects.
Some developers are surprised that those methods are not part of the Ruby standard library, and I must admit that I often miss them when writing pure Ruby code. Let's examine the #blank?
method to understand its magic.
If you look into the Rails source, you will notice the Active Support module. This module is responsible for overwriting standard Ruby classes like String, Array, etc. These classes are called core extensions and are grouped in a special directory. There is no one method blank?
that works for all types of objects; each class is updated independently.
It happens in the activesupport/lib/active_support/core_ext/object/blank.rb
definition file that overwrites many different classes. For string, nil is not the only representation we consider as a blank string. We also have a string that is filled only with spaces.
class String
BLANK_RE = /\A[[:space:]]*\z/
ENCODED_BLANKS = Concurrent::Map.new do |h, enc|
h[enc] = Regexp.new(BLANK_RE.source.encode(enc), BLANK_RE.options | Regexp::FIXEDENCODING)
end
def blank?
empty? ||
begin
BLANK_RE.match?(self)
rescue Encoding::CompatibilityError
ENCODED_BLANKS[self.encoding].match?(self)
end
end
end
The standard #empty?
method easily handles blank strings but cannot detect empty spaces. It's used first because regular expressions are much more expensive, and it does not make sense to execute them if we deal with empty strings.
For some classes, we don't have to implement #blank?
method, we can make it an alias for #empty?
as they are working as expected. For some objects like Time
or Numeric
this method will always return false as those objects are always considered present.
When exploring #blank?
and #present?
it's also worth mentioning the #presence
method, which is useful when we want to the assignment, but in case of blank value, we want to assign some other value without the if expression:
name = current_user.full_name.presence || 'Unknown'
The source code is straightforward:
def presence
self if present?
end
The core extension section in the ActiveSupport module is a valuable source of information when you want to understand how Ruby on Rails works under the hood. In addition to the #blank?
, #present?
, and #presence
methods, it contains more magic that is present in multiple Rails mechanisms.
Master the art of database optimization in Rails applications. Learn proven techniques to improve your Ruby on Rails application performance.
I'm sure you have already got used to the way how we can manipulate configuration data for Rubygems, and it has become a standard to use the following format:
UsefulGem.configure do |config|
config.api_key = 'secret'
end
It's a typical representation of the DSL in Ruby. We are not doing anything advanced; we only assign values to variables later used in the gem source to alter the behavior of the library.
In the above example, we yield the instance of the configuration class and can set the value for attributes just like we would for any other class. Here is the simple implementation:
module UsefulGem
class << self
attr_accessor :configuration
def configure
self.configuration ||= Configuration.new
yield(configuration)
end
end
class Configuration
attr_accessor :api_key
end
end
Once we set the configuration in the initializer, access to the configuration values is as simple as calling UsefulGem.configuration.api_key
anywhere in the application.
Duck typing is one of the features that makes Ruby such a flexible programming language. One of the examples when you see it in action is when you use some libraries to perform HTTP requests:
response = SomeGem.request('https://example.com')
response.status # => 200
response.body # => 'Hello world'
response.class # => SomeGem::Response
puts response # => 'status: 200, body: Hello world'
How is that even possible? The value assigned to the response variable is not a String, which we can see, yet it behaves like a String. It's because it implements the to_s
definition, which tells Ruby how this object should behave when we treat it like a String:
module SomeGem
class Response
attr_reader :status, :body
def initialize(status:, body:)
@status = status
@body = body
end
def to_s
"status: #{@status}, body: #{@body}"
end
end
end
How about the domain objects behaving like an Array? Imagine we have a garage, and we want to park cars easily:
garage = Garage.new
garage.cars << Car.new('Mercedes')
garage.cars.class # => CarsCollection
The CarsCollection
behaves like an array:
class CarsCollection
def initialize
@cars = []
end
def <<(car)
@cars << car
end
def to_a
@cars
end
end
class Garage
attr_reader :cars
def initialize
@cars = CarsCollection.new
end
end
There are probably endless possibilities to build your own classes and then make them behave like standard Ruby classes like String
or Array
. Thanks to such an approach, developers can interact with objects using the old, well-known method.
The methods that are not defined in a standard way are a blessing when it comes to flexibility and a curse when it comes to debugging them; good luck with searching for their definition. Ruby on Rails is full of them, and they are among the most remarkable features of the framework.
Let's have a look at the method that allows us to find an article using it's title:
Article.find_by_title('Ruby on Rails')
There is no find_by_title
method defined by the source code. However, the magic behind this method is easily explainable. Let's explain it by creating a dynamic method that allows us to compare values with syntax sugar:
article = Article.new
article.same_as_title?('Ruby on Rails')
This can be achieved by altering the method_missing
method:
module SameAsMethod
private
def method_missing(method_name, *args, &block)
attribute_name = method_name.to_s.match(/same_as_(.*)\?/)&.captures&.first
if !attribute_name.nil? && instance_variable_defined?("@#{attribute_name}")
instance_variable_get("@#{attribute_name}") == args.first
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_name.to_s.match?(/same_as_(.*)\?/) || super
end
end
We can now include the module in the Article
class to make a test:
class Article
include SameAsMethod
attr_reader :title, :category, :author
def initialize(title:, category:, author:)
@title = title
@category = category
@author = author
end
end
Let's check if it's working as expected:
article = Article.new(title: 'Ruby on Rails', category: 'Programming', author: 'John')
article.same_as_author?('Tim') # => false
article.same_as_title?('Ruby on Rails') # => true
When you try to execute a method that does not exist, the method_missing
is triggered. We can then ensure that we support the dynamic method name and create it in "fly". As you may notice, I also defined the respond_to_missing?
method which allows us to ensure that the object responds to the method:
article = Article.new(...)
article.respond_to?(:same_as_author?) # => true
When you redefine method_missing
, always define respond_to_missing?
with corresponding logic. Without it, you won't be able to call article.method(:same_as_author?)
as well.
Master the art of database optimization in Rails applications. Learn proven techniques to improve your Ruby on Rails application performance.
One of the most incredible things about Rails is that you can call 3.hours
, 1.month
or 15.minutes
and get a time object or perform time operations with the mentioned code. It's readable, and you don't have to memorize some special classes. Yes, it's related to duck typing because integers in Ruby don't have methods like month, hours, or minutes.
If you inspect similar instructions, you will notice they are instances of ActiveSupport::Duration
class. This class is responsible for handling the magic. And the magic itself is added to the standard types quite simply:
class Numeric
def seconds
ActiveSupport::Duration.seconds(self)
end
end
So, if you want to create three instances of Car
class with the 3.cars
fancy syntax, you can implement it quickly:
class Car; end
class Integer
def cars
Array.new(self) { Car.new }
end
end
And then quickly create as many instances as you want:
3.cars
# => [#<Car:0x000000010824ff50>, #<Car:0x000000010824fed8>, #<Car:0x000000010824feb0>]
As simple as that. You might be wondering the same, but when you call 3.seconds
you get the ActiveSupport::Duration
instance, but in the console, you will see only 3 seconds
:
3.seconds
# => 3 seconds
I was wondering how is that possible until I saw the core definition for this part of the Rails codebase, and it is possible thanks to defining the inspect
method:
class City
def initialize(name)
@name = name
end
def inspect
"#{@name} city"
end
end
It's now possible:
City.new('Paris')
# => Paris city