Ruby has several terms that sound similar but serve different purposes. If youโve ever been confused by things like Procfile, Rakefile, Rack, and Rake, this guide will clarify them all. Plus, we’ll cover additional tricky concepts you might have overlooked!
1. Procfile
What is it?
A Procfile is a text file used in deployment environments (like Heroku and Kamal) to specify how your application should be started.
Where is it used?
Platforms like Heroku, Kamal, and Foreman use Procfile to define process types (like web servers and workers).
We can also insert array elements into another Array. In the example below, odds elements are added to the numbers Array, starting from the position where *odds is called.
Example: Combining Required and Optional Keyword Arguments
def greet(name:, age: nil)
puts "Hello, #{name}!"
puts "You are #{age} years old." if age
end
greet(name: "Alice", age: 25)
# Output:
# Hello, Alice!
# You are 25 years old.
Example: Capturing Extra Keyword Arguments with **options
The ** operator captures any additional keyword arguments passed to the method into a hash.
Ruby has many terms that seem similar but have distinct uses. By understanding Procfile, Rake, Rack, and middleware in Rails 8, youโll have a much clearer picture of how Ruby applications work under the hood. If you’re working on a Rails 8 app, take some time to explore these concepts furtherโthey’ll definitely make your life easier!
Meanwhile we are setting up some UI for our app using Tailwind CSS, I have uploaded 2 images to our product in the rich text editor. Let’s discuss about this in this post.
Understanding Active Storage in Rails 8: A Deep Dive into Image Uploads
In our Rails 8 application, we recently tested uploading two images to a product using the rich text editor. This process internally triggers several actions within Active Storage. Let’s break down what happens behind the scenes.
How Active Storage Handles Image Uploads
When an image is uploaded, Rails 8 processes it through Active Storage, creating a new blob entry and storing it in the disk service. The following request is fired:
Processing by ActiveStorage::DirectUploadsController#create as JSON
Parameters: {"blob" => {"filename" => "floral-kurtha.jpg", "content_type" => "image/jpeg", "byte_size" => 107508, "checksum" => "GgNgNxxxxxxxjdPOLw=="}}
This request initiates a database entry in active_storage_blobs:
This process triggers the ActiveStorage::DiskController, handling file storage via a PUT request:
Started PUT "/rails/active_storage/disk/eyJfcmFpbHMiOxxxxx"
Disk Storage (0.9ms) Uploaded file to key: hut9d0zxssxxxxxx
Completed 204 No Content in 96ms
Retrieving Images from Active Storage
After successfully storing the file, the application fetches the image via a GET request:
Started GET "/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOxxxxxxxxxxfQ==--f9c556012577xxxxxxxxxxxxfa21/floral-kurtha-2.jpg"
This request is handled by:
Processing by ActiveStorage::Blobs::RedirectController#show as JPEG
The file is then served via the ActiveStorage::DiskController#show:
Redirected to http://localhost:3000/rails/active_storage/disk/eyJfcmFpbHMiOnsiZGxxxxxxxxxd048aae4ab5c30/floral-kurtha-2.jpg
Updating Records with Active Storage Attachments
When updating a product, the system also updates its associated images. The following Active Storage updates occur:
UPDATE "action_text_rich_texts" SET "body" = .... WHERE "action_text_rich_texts"."id" = 1
UPDATE "active_storage_blobs" SET "metadata" = '{"identified":true}' WHERE "active_storage_blobs"."id" = 3
INSERT INTO "active_storage_attachments" ("name", "record_type", "record_id", "blob_id", "created_at") VALUES ('embeds', 'ActionText::RichText', 1, 3, '2025-03-31 11:46:13.464597')
Additionally, Rails updates the updated_at timestamp of the associated records:
UPDATE "products" SET "updated_at" = '2025-03-31 11:46:13.523640' WHERE "products"."id" = 1
Best Practices for Active Storage in Rails 8
Use Direct Uploads: This improves performance by uploading files directly to cloud storage (e.g., AWS S3, Google Cloud Storage) instead of routing them through your Rails server.
Attach Images Efficiently: Use has_one_attached or has_many_attached for file associations in models.
Avoid Serving Files via Rails: Use a CDN or proxy service to serve images instead of relying on Rails controllers.
Clean Up Unused Blobs: Regularly remove orphaned blob records using ActiveStorage::Blob.unattached.destroy_all.
Optimize Image Processing: Use variants (image.variant(resize: "300x300").processed) to generate resized images efficiently.
In Rails 8, Active Storage uses two main tables for handling file uploads:
1. active_storage_blobs Table
This table stores metadata about the uploaded files but not the actual files. Each row represents a unique file (or “blob”) uploaded to Active Storage.
Columns in active_storage_blobs Table:
id โ Unique identifier for the blob.
key โ A unique key used to retrieve the file.
filename โ The original name of the uploaded file.
content_type โ The MIME type (e.g., image/jpeg, application/pdf).
metadata โ JSON data storing additional information (e.g., width/height for images).
service_name โ The storage service (e.g., local, amazon, google).
byte_size โ File size in bytes.
checksum โ A checksum to verify file integrity.
created_at โ Timestamp when the file was uploaded.
๐ Purpose: This table allows a single file to be attached to multiple records without duplicating the file itself.
Why Does Rails Need Both Tables?
Separation of Concerns:
active_storage_blobstracks the files themselves.
active_storage_attachmentslinks them to models.
Efficient File Management:
The same file can be used in multiple places without storing it multiple times.
If a file is no longer attached to any record, Rails can remove it safely.
Supports Different Attachments:
A model can have different types of attachments (avatar, cover_photo, documents).
A single model can have multiple files attached (has_many_attached).
Example Usage in Rails 8
class User < ApplicationRecord
has_one_attached :avatar # Single file
has_many_attached :photos # Multiple files
end
When a file is uploaded, an entry is added to active_storage_blobs, and an association is created in active_storage_attachments.
How Rails Queries These Tables
user.avatar # Fetches from `active_storage_blobs` via `active_storage_attachments`
user.photos.each { |photo| puts photo.filename } # Fetches multiple attached files
Conclusion
Rails 8 uses two tables to decouple file storage from model associations, enabling better efficiency, flexibility, and reusability. This structure allows models to reference files without duplicating them, making Active Storage a powerful solution for file management in Rails applications. ๐
Where Are Files Stored in Rails 8 by Default?
By default, Rails 8 stores uploaded files using Active Storage’s disk service, meaning files are saved in the storage/ directory within your Rails project.
Default Storage Location:
Files are stored in: storage/ โโโ cache/ (temporary files) โโโ store/ (permanent storage) โโโ variant/ (image transformations like resizing)
The exact file path inside storage/ is determined by the key column in the active_storage_blobs table. For example, if a blob entry has: key = 'xyz123abcd' then the file is stored at: storage/store/xyz123abcd
How to Change the Storage Location?
You can configure storage in config/storage.yml. For example:
local:
service: Disk
root: <%= Rails.root.join("storage") %>
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
amazon:
service: S3
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
region: us-east-1
bucket: your_own_bucket-<%= Rails.env %>
Then, update config/environments/development.rb (or production.rb) to use:
config.active_storage.service = :local # or :amazon for S3
How to Get the Stored File Path in Rails 8 Active Storage
Since Rails stores files in a structured directory inside storage/, the actual file path can be determined using the key stored in the active_storage_blobs table.
Get the File Path in Local Storage
If you’re using the Disk service (default for development and test), you can retrieve the stored file path manually:
Files are stored in the storage/ directory by default.
Use rails_blob_url or service_url to get an accessible URL.
Use variant to generate resized versions.
For production, it’s best to use a cloud storage service like Amazon S3.
Understanding has_one_attached and has_many_attached in Rails 8
Rails 8 provides a built-in way to handle file attachments through Active Storage. The key methods for attaching files to models are:
has_one_attached โ For a single file attachment.
has_many_attached โ For multiple file attachments.
Let’s break down what they do and why they are useful.
1. has_one_attached
This is used when a model should have a single file attachment. For example, a User model may have only one profile picture.
Usage:
class User < ApplicationRecord
has_one_attached :avatar
end
How It Works:
When you upload a file, Active Storage creates an entry in the active_storage_blobs table.
The active_storage_attachments table links this file to the record.
If a new file is attached, the old one is automatically replaced.
Example: Attaching and Displaying an Image
user = User.find(1)
user.avatar.attach(io: File.open("/path/to/avatar.jpg"), filename: "avatar.jpg", content_type: "image/jpeg")
# Checking if an avatar exists
user.avatar.attached? # => true
# Displaying the image in a view
<%= image_tag user.avatar.variant(resize: "100x100").processed if user.avatar.attached? %>
2. has_many_attached
Use this when a model can have multiple file attachments. For instance, a Product model may have multiple images.
Usage:
class Product < ApplicationRecord
has_many_attached :images
end
How It Works:
Multiple files can be attached to a single record.
Active Storage tracks all file uploads in the active_storage_blobs and active_storage_attachments tables.
Deleting an attachment removes it from storage.
Example: Attaching and Displaying Multiple Images
product = Product.find(1)
product.images.attach([
{ io: File.open("/path/to/image1.jpg"), filename: "image1.jpg", content_type: "image/jpeg" },
{ io: File.open("/path/to/image2.jpg"), filename: "image2.jpg", content_type: "image/jpeg" }
])
# Checking if images exist
product.images.attached? # => true
# Displaying all images in a view
<% if product.images.attached? %>
<% product.images.each do |image| %>
<%= image_tag image.variant(resize: "200x200").processed %>
<% end %>
<% end %>
Benefits of Using has_one_attached & has_many_attached
Simplifies File Attachments โ Directly associates files with Active Record models.
No Need for Extra Tables โ Unlike some gems (e.g., CarrierWave), Active Storage doesn’t require additional tables for storing file paths.
Easy Cloud Storage Integration โ Works seamlessly with Amazon S3, Google Cloud Storage, and Azure.
Variant Processing โ Generates resized versions of images using variant (e.g., thumbnails).
Automatic Cleanup โ Old attachments are automatically removed when replaced.
Final Thoughts
Active Storage in Rails 8 provides a seamless way to manage file uploads, integrating directly with models while handling storage efficiently. By understanding how it processes uploads internally, we can better optimize performance and ensure a smooth user experience.
In an upcoming blog, we’ll dive deeper into Turbo Streams and how they enhance real-time updates in Rails applications.
In the first part of this guide, we covered setting up a Rails 8 app with essential configurations. In this follow-up, weโll go over optimizing command-line usage, setting up VS Code for development, running migrations, styling the app, and enabling Action Text.
1. Optimizing Command-Line Usage with Aliases
One of the best ways to speed up development is to create shortcuts for frequently used commands. You can do this by adding aliases to your shell configuration.
Steps to Add an Alias:
Open your shell configuration file: vim ~/.zshrc
Search for the alias section: <esc> / alias <enter>
Add your alias: alias gs="git status"
Save and exit: <esc> :wq
Reload your configuration: source ~/.zshrc
Use your new alias: gs
This method saves time by allowing you to run frequently used commands more quickly.
2. Using Terminal Efficiently in VS Code
By default, VS Code uses `Ctrl + “ to toggle the terminal, which may not be intuitive. You can change this shortcut:
Open VS Code.
Go to Settings โ Keyboard Shortcuts.
Search for Toggle Terminal.
Click Edit and change it to Ctrl + Opt + T for easier access.
3. Setting Up RuboCop in VS Code
RuboCop ensures your Ruby code follows best practices. Hereโs how to set it up:
Checking RuboCop from the Terminal:
rubocop .
VS Code Setup:
Open Command Palette (Cmd + Shift + P) and search for “Lint by RuboCop”.
Go to Extensions Tab and install “VS Code RuboCop”.
In VS Code Settings, search for “Rubocop” and check Ruby -> Rubocop -> Execute Path.
Find the RuboCop installation path: whereis rubocop Example output: ~/.local/share/mise/installs/ruby/3.4.1/bin/rubocop/
Update the Execute Path in VS Code to: ~/.local/share/mise/installs/ruby/3.4.1/bin/
If RuboCop still returns an empty output, check .rubocop.yml in your project: ~/rails/design_studio/.rubocop.yml
If the issue persists, ensure the gem is installed: gem install rubocop
Restart VS Code from the Rails project root: code .
For more details, check the official documentation: RuboCop Usage
4. Running Migrations and Starting the Server
Running Migrations:
rails db:migrate -t
You can check the file: db/schema.rb You can see the active storage tables for attachments and other info.
Ruby on Rails 8 introduces several improvements that make development easier, more secure, and more maintainable. In this guide, we’ll walk through setting up a new Rails 8 application while noting the significant configurations and features that come out of the box.
Rails 8 provides new shortcuts and syntax improvements for database migrations:
NOT NULL Constraints with ! Shortcut
You can impose NOT NULL constraints directly from the command line using !:
# Example for not null constraints:
โ rails generate model User name:string!
Type Modifiers in Migrations
Rails 8 allows passing commonly used type modifiers directly via the command line. These modifiers are enclosed in curly braces {} after the field type.
# Example for model generation:
โ rails generate model Product name:string description:text
# Example for passing modifiers:
โ rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}
Generating a Scaffold for the Product Model
Let’s generate a complete scaffold for our Product model:
The rails g resource command is a powerful way to generate models, controllers, migrations, and routes all in one go. This is particularly useful when setting up RESTful resources in a Rails application.
Basic Syntax
โ rails g resource product
This command creates the necessary files for a new resource, including:
A model (app/models/product.rb)
A migration file (db/migrate/)
A controller (app/controllers/product_controller.rb)
Routes in config/routes.rb
A test file (test/controllers/product_controller_test.rb or spec/)
Example Usage
To generate a Post resource with attributes:
โ rails g resource Product title:string! description:text brand:references
This will:
Create a Product model with title and description attributes.
Add a brand_id foreign key as a reference.
Apply a NOT NULL constraint on title (! shortcut).
Generate a corresponding migration file.
Set up routes automatically (resources :products).
Running the Migration
After generating a resource, apply the migration to update the database:
โ rails db:migrate
Difference Between resource and scaffold
Rails provides both rails g resource and rails g scaffold, but they serve different purposes:
Feature
rails g resource
rails g scaffold
Generates a Model
โ
โ
Generates a Migration
โ
โ
Generates a Controller
โ (empty actions)
โ (full CRUD actions)
Generates Views (HTML/ERB)
โ
โ (index, show, new, edit, etc.)
Generates Routes
โ
โ
Generates Helper Files
โ
โ
Generates Tests
โ
โ
rails g resource is minimalโit generates only essential files without view templates. It’s useful when you want more control over how your views and controller actions are built.
rails g scaffold is more opinionated and generates full CRUD functionality with prebuilt forms and views, making it ideal for rapid prototyping.
If you need full CRUD functionality quickly, use scaffold. If you prefer a leaner setup with manual control over implementation, use resource.
Conclusion
Rails 8 significantly enhances the development experience with built-in security tools, CI/CD workflows, streamlined deployment via Kamal, and modern front-end support with Turbo & Stimulus. It also improves caching, background jobs, and real-time features with Solid tools.
These improvements make Rails 8 a robust framework for modern web applications, reducing the need for additional dependencies while keeping development efficient and secure.
Active Record (AR) is the heart of Ruby on Rails when it comes to database interactions. Writing efficient and readable queries is crucial for application performance and maintainability. This guide will help you master Active Record queries with real-world examples and best practices.
Setting Up a Sample Database
To demonstrate complex Active Record queries, letโs create a Rails app with a sample database structure containing multiple tables.
Generate Models & Migrations
rails new MyApp --database=postgresql
cd MyApp
rails g model User name:string email:string
rails g model Post title:string body:text user:references
rails g model Comment body:text user:references post:references
rails g model Category name:string
rails g model PostCategory post:references category:references
rails g model Like user:references comment:references
rails db:migrate
Database Schema Overview
users: Stores user information.
posts: Stores blog posts written by users.
comments: Stores comments on posts, linked to users and posts.
categories: Stores post categories.
post_categories: Join table for posts and categories.
likes: Stores likes on comments by users.
Basic Active Record Queries
1. Fetching All Records
User.all # Returns all users (Avoid using it directly on large datasets as it loads everything into memory)
โ ๏ธ User.all can lead to performance issues if the table contains a large number of records. Instead, prefer pagination (User.limit(100).offset(0)) or batch processing (User.find_each).
2. Finding a Specific Record
User.find(1) # Finds a user by ID
User.find_by(email: 'john@example.com') # Finds by attribute
3. Filtering with where vs having
Post.where(user_id: 2) # Fetch all posts by user with ID 2
Difference between where and having:
where is used for filtering records before grouping.
having is used for filtering after group operations.
Example:
Post.group(:user_id).having('COUNT(id) > ?', 5) # Users with more than 5 posts
4. Ordering Results
User.order(:name) # Order users alphabetically
Post.order(created_at: :desc) # Order posts by newest first
5. Limiting Results
Post.limit(5) # Get the first 5 posts
6. Selecting Specific Columns
User.select(:id, :name) # Only fetch ID and name
7. Fetching Users with a Specific Email Domain
User.where("email LIKE ?", "%@gmail.com")
8. Fetching the Most Recent Posts
Post.order(created_at: :desc).limit(5)
9. Using pluck for Efficient Data Retrieval
User.pluck(:email) # Fetch only emails as an array
10. Checking if a Record Exists Efficiently
User.exists?(email: 'john@example.com')
11. Including Associations (eager loading to avoid N+1 queries)
13. Fetching Users, Their Posts, and the Count of Comments on Each Post
User.joins(posts: :comments)
.group('users.id', 'posts.id')
.select('users.id, users.name, posts.id AS post_id, COUNT(comments.id) AS comment_count')
.order('comment_count DESC')
Importance of inverse_of in Model Associations
What is inverse_of?
The inverse_of option in Active Record associations helps Rails correctly link objects in memory, avoiding unnecessary database queries and ensuring bidirectional association consistency.
Example Usage
class User < ApplicationRecord
has_many :posts, inverse_of: :user
end
class Post < ApplicationRecord
belongs_to :user, inverse_of: :posts
end
Why Use inverse_of?
Performance Optimization: Prevents extra queries by using already loaded objects.
Ensures Data Consistency: Updates associations without additional database fetches.
Enables Nested Attributes: Helps when using accepts_nested_attributes_for.
Example:
user = User.new(name: 'Alice')
post = user.posts.build(title: 'First Post')
post.user == user # True without needing an additional query
Best Practices to use in Rails Projects
1. Using Scopes for Readability
class Post < ApplicationRecord
scope :recent, -> { order(created_at: :desc) }
end
Post.recent.limit(10) # Fetch recent posts
2. Using find_each for Large Datasets
User.find_each(batch_size: 100) do |user|
puts user.email
end
3. Avoiding SELECT * for Performance
User.select(:id, :name).load
4. Avoiding N+1 Queries with includes
Post.includes(:comments).each do |post|
puts post.comments.count
end
Conclusion
Mastering Active Record queries is essential for writing performant and maintainable Rails applications. By using joins, scopes, batch processing, and eager loading, you can write clean and efficient queries that scale well.
Do you have any favorite Active Record query tricks? Share them in the comments!
Ruby is known for its elegant and expressive syntax, making it one of the most enjoyable programming languages to work with. In this blog post, we will explore some interesting aspects of Ruby, including syntax sugar, operator methods, useful array operations, and handy built-in methods.
Ruby Syntax Sugar
Ruby provides several shorthand notations that enhance code readability and reduce verbosity. Let’s explore some useful syntax sugar techniques.
Shorthand for Array and Symbol Arrays
Instead of manually writing arrays of strings or symbols, Ruby provides %w and %i shortcuts.
words = %w[one two three] # => ["one", "two", "three"]
symbols = %i[one two three] # => [:one, :two, :three]
Many mathematical and array operations in Ruby are actually method calls, thanks to Ruby’s message-passing model.
puts 1.+(2) # => 3 (Same as 1 + 2)
Array Indexing and Append Operations
words = %w[zero one two three four]
puts words.[](2) # => "two" (Equivalent to words[2])
words.<<('five') # => ["zero", "one", "two", "three", "four", "five"] (Equivalent to words << "five")
Avoiding Dot Notation with Operators
# Bad practice
num = 10
puts num.+(5) # Avoid this syntax
# Good practice
puts num + 5 # Preferred
Using a linter like rubocop can help enforce best practices in Ruby code.
none? Method in Ruby Arrays
The none? method checks if none of the elements in an array satisfy a given condition.
numbers = [1, 2, 3, 4, 5]
puts numbers.none? { |n| n > 10 } # => true (None are greater than 10)
puts numbers.none?(&:odd?) # => false (Some numbers are odd)
This method is useful for quickly asserting that an array does not contain specific values.
Exponent Operator (**) and XOR Operator (^)
Exponentiation
Ruby uses ** for exponentiation instead of ^ (which performs a bitwise XOR operation).
puts 2 ** 3 # => 8 (2 raised to the power of 3)
puts 10 ** 0.5 # => 3.162277660168379 (Square root of 10)
XOR Operator
In Ruby, ^ is used for bitwise XOR operations, which differ from exponentiation.
This is particularly useful for cleaning up user input or text from external sources.
Conclusion
Ruby’s syntax sugar, operator methods, and built-in methods make code more readable, expressive, and powerful. By leveraging these features effectively, you can write clean, efficient, and maintainable Ruby code.
Ruby on Rails is known for its developer-friendly syntax and expressive code structure. One of the key reasons behind this elegance is its use of Domain-Specific Languages (DSLs). DSLs make Rails configurations, routes, and testing more intuitive by allowing developers to write code that reads like natural language.
In this blog post, we’ll explore what DSLs are, how Rails implements them, and why they make development in Rails both powerful and enjoyable.
What is a DSL?
A Domain-Specific Language (DSL) is a specialized language designed to solve problems in a specific domain. Unlike general-purpose languages (like Ruby or Java), a DSL provides a more concise and readable syntax for a particular task.
Two types of DSLs exist:
Internal DSLs: Written using an existing programming languageโs syntax (e.g., Rails DSLs in Ruby).
External DSLs: Separate from the host language and require a custom parser (e.g., SQL, Regular Expressions).
Rails uses Internal DSLs to simplify web development. Letโs explore some core DSLs in Rails and how they work under the hood.
1. Routes in Rails: A Classic Example of DSL
In config/routes.rb, Rails provides a DSL to define application routes in a clear and structured way.
Example:
Rails.application.routes.draw do
resources :users do
resources :posts
end
get '/about', to: 'pages#about'
root 'home#index'
end
How Does This Work?
resources :users automatically generates RESTful routes for UsersController.
get '/about', to: 'pages#about' maps a GET request to the about action in PagesController.
root 'home#index' sets the default landing page.
Why Use a DSL for Routes?
Concise & Readable: Avoids manually defining each route.
Expressive Syntax: Reads like a structured list of instructions.
Rails extends DSL capabilities with ActiveSupport::Concern, which allows modular mixins in models and controllers.
Example:
module Trackable
extend ActiveSupport::Concern
included do
before_save :track_changes
end
private
def track_changes
puts "Tracking changes!"
end
end
class User < ApplicationRecord
include Trackable
end
How This Works:
included do ... end executes code when the module is included in a class.
before_save :track_changes hooks into the Rails lifecycle to run before saving a record.
Why a DSL for Mixins?
Encapsulation: Keeps related logic together.
Reusability: Can be included in multiple models.
Cleaner Code: Removes redundant callbacks in models.
Conclusion: Why Rails Embraces DSLs
DSLs in Rails make the framework expressive, flexible, and developer-friendly. They provide:
By leveraging DSLs, Rails makes web development intuitive, allowing developers to focus on building great applications rather than writing repetitive code.
So next time you’re defining routes, configuring settings, or writing tests in Railsโremember, you’re using DSLs that make your life easier!
Ruby is often celebrated for its expressive and dynamic nature, and one of its most fascinating yet under-appreciated feature is message passing. Unlike many other languages that focus purely on method calls, Ruby inherits the concept of sending messages from Smalltalk and embraces it throughout the language.
Let’s explore what message passing is, why Ruby uses it, and how you can leverage it effectively in your code.
What is Message Passing in Ruby?
In Ruby, every method call is actually a message being sent to an object. When you invoke a method on an object, Ruby sends a message to that object, asking it to execute a particular behavior.
Example:
str = "Hello, Ruby!"
puts str.reverse # Standard method call
Behind the scenes, Ruby treats this as sending the :reverse message to the str object:
puts str.send(:reverse) # Equivalent to str.reverse
This concept extends beyond just methodsโit applies to variable access and operators too! Even str.length is just Ruby sending the :length message to str.
Why Does Ruby Use Message Passing?
Message passing provides several advantages:
Encapsulation and Flexibility
Instead of directly accessing methods, sending messages allows objects to decide how they respond to method calls.This abstraction makes code more modular and adaptable, allowing behavior to change at runtime without modifying existing method definitions.
class DynamicResponder
def method_missing(name, *args)
"I received a message: #{name}, but I don't have a method for it."
end
end
obj = DynamicResponder.new
puts obj.some_method # => "I received a message: some_method, but I don't have a method for it."
2. Dynamic Method Invocation
You can invoke methods dynamically at runtime using symbols instead of hardcoded method names.
Ruby allows defining methods dynamically using define_method, making it easier to create flexible APIs.
class Person
[:first_name, :last_name].each do |method|
define_method(method) do |value|
instance_variable_set("@#{method}", value)
end
end
end
p = Person.new
p.first_name("John")
p.last_name("Doe")
Using send to Pass Messages Dynamically
The send method allows you to invoke methods dynamically using symbols or strings representing method names.
This ensures safe message passing by verifying that an object responds to a method before calling it.
Example 3: Avoiding Private Method Calls with public_send
class Secret
private
def hidden_message
"This is private!"
end
end
secret = Secret.new
puts secret.send(:hidden_message) # Works! (bypasses visibility)
puts secret.public_send(:hidden_message) # Error! (Respects visibility)
Use public_send to ensure that only public methods are invoked dynamically.
Defining Methods Dynamically with define_method
Ruby also allows defining methods dynamically at runtime using define_method:
Example 1: Creating Methods on the Fly
class Person
[:first_name, :last_name].each do |method|
define_method(method) do |value|
instance_variable_set("@#{method}", value)
end
end
end
p = Person.new
p.first_name("John")
p.last_name("Doe")
This creates first_name and last_name methods dynamically.
Example 2: Generating Getters and Setters
class Dynamic
attr_accessor :data
end
obj = Dynamic.new
obj.data = "Dynamic Method"
puts obj.data # => "Dynamic Method"
Handling Undefined Methods with method_missing
If an object receives a message (method call) that it does not understand, Ruby provides a safety net: method_missing.
Example: Catching Undefined Method Calls
class Robot
def method_missing(name, *args)
"I donโt know how to #{name}!"
end
end
r = Robot.new
puts r.dance # => "I donโt know how to dance!"
puts r.fly # => "I donโt know how to fly!"
This feature is commonly used in DSLs (Domain Specific Languages) to handle flexible method invocations.
How Ruby Differs from Other Languages
Many languages support method calls, but few treat method calls as message passing the way Ruby does. For example:
Python: Uses getattr(obj, method_name) for dynamic calls, but lacks an equivalent to method_missing.
JavaScript: Uses obj[method_name]() for indirect invocation but doesnโt treat calls as messages.
Java: Requires reflection (Method.invoke), making it less fluid than Ruby.
Smalltalk: The original message-passing language, which inspired Rubyโs approach.
When to Use Message Passing Effectively
โ Use send for dynamic method invocation:
Useful for reducing boilerplate in frameworks like Rails.
Ideal when working with DSLs or metaprogramming scenarios.
โ Use respond_to? before send to avoid errors:
Ensures that the object actually has the method before attempting to call it.
โ Use define_method for dynamic method creation:
Helps in cases where multiple similar methods are needed.
โ Use method_missing cautiously:
Can be powerful, but should be handled carefully to avoid debugging nightmares.
Final Thoughts
Rubyโs message passing mechanism is one of the reasons it excels in metaprogramming and dynamic method invocation. Understanding this feature allows developers to write more flexible and expressive code while keeping it clean and readable. The best example using this features you can see here: https://github.com/rails/rails . None other than Rails Framework itself!!
By embracing message passing, you can unlock new levels of code abstraction, modularity, and intelligent method handling in your Ruby projects.
Ruby is well known for its flexibility and expressive syntax. One of its powerful features is the ability to share functionality using modules. While many languages offer mixins, Ruby stands out with three key methods: include, extend, and prepend. Understanding these methods can significantly improve code reuse and clarity. Letโs explore each with examples!
include โ Adds Module Methods as Instance Methods
When you use include in a class, it injects the moduleโs methods as instance methods of the class.
Example: Using include for instance methods
module Greet
def hello
"Hello from Greet module!"
end
end
class User
include Greet
end
user = User.new
puts user.hello # => "Hello from Greet module!"
โ Scope: The methods from Greet become instance methods in User.
extend โ Adds Module Methods as Class Methods
When you use extend in a class, it injects the moduleโs methods as class methods.
Example: Using extend for class methods
module Greet
def hello
"Hello from Greet module!"
end
end
class User
extend Greet
end
puts User.hello # => "Hello from Greet module!"
โ Scope: The methods from Greet become class methods in User, instead of instance methods.
prepend โ Adds Module Methods Before Instance Methods
prepend works like include, but the methods from the module take precedence over the classโs own methods.
Example: Using prepend to override instance methods
module Greet
def hello
"Hello from Greet module!"
end
end
class User
prepend Greet
def hello
"Hello from User class!"
end
end
user = User.new
puts user.hello # => "Hello from Greet module!"
โ Scope: The methods from Greet override User‘s methods because prepend places Greetbefore the class in the method lookup chain.
Method Lookup Order
Understanding how Ruby resolves method calls is essential when using these techniques.
The method lookup order is as follows:
Prepend modules (highest priority)
Instance methods in the class itself
Include modules
Superclass methods
Object, Kernel, BasicObject
To visualize this, use the ancestors method:
class User
prepend Greet
end
puts User.ancestors
# => [Greet, User, Object, Kernel, BasicObject]
How Ruby Differs from Other Languages
Ruby is unique in dynamically modifying method resolution order (MRO) using prepend, include, and extend.
In Python, multiple inheritance is used with super(), but prepend-like behavior does not exist.
JavaScript relies on prototypes instead of module mixins, making Rubyโs approach cleaner and more structured.
Quick Summary
Keyword
Injects Methods As
Works On
Takes Precedence?
include
Instance methods
Instances
No
extend
Class methods
Classes
N/A
prepend
Instance methods
Instances
Yes (overrides class methods)
By understanding and using include, extend, and prepend, you can make your Ruby code more modular, reusable, and expressive. Master these techniques to level up your Ruby skills!
Ruby is an elegant and expressive language that stands out due to its simplicity and readability. While its syntax is designed to be natural and easy to use, Ruby also has several powerful and unique features that make it a joy to work with. Let’s explore some of the fascinating aspects of Ruby that set it apart from other programming languages.
Everything is an Expression
Unlike many other languages where statements exist separately from expressions, in Ruby, everything is an expression. Every line of code evaluates to something, even function definitions. For example:
result = def greet
"Hello, World!"
end
puts result # => :greet
Here, defining the greet method itself returns a symbol representing its name, :greet. This feature makes metaprogramming and introspection very powerful in Ruby.
Other examples:
Conditional expressions return values:
value = if 10 > 5
"Greater"
else
"Smaller"
end
puts value # => "Greater"
Loops also evaluate to a return value:
result = while false
"This won't run"
end
puts result # => nil
result = until true
"This won't run either"
end
puts result # => nil
Assignment is an expression:
x = y = 10 + 5
puts x # => 15
puts y # => 15
Case expressions return values:
day = "Sunday"
message = case day
when "Monday"
"Start of the week"
when "Friday"
"Almost weekend!"
when "Sunday"
"Time to relax!"
else
"Just another day"
end
puts message # => "Time to relax!"
This characteristic of Ruby allows concise and elegant code that minimizes the need for temporary variables and makes code more readable.
The send Method for Dynamic Method Invocation
While Ruby does not allow storing functions in variables (like JavaScript or Python), you can hold their identifiers as symbols and invoke them dynamically using send:
Why does Ruby use .send instead of .call? The call method is used for Proc and lambda objects, whereas send is a general method that allows invoking methods dynamically on an object. This also enables calling private methods.
So the question is can we restrict the programmer calling private methods with send? Let’s look at some examples how we can do that.
1. Use public_send Instead of send
The public_send method only allows calling public methods, preventing invocation of private methods.
class Demo
private
def secret_method
"This is private!"
end
end
d = Demo.new
puts d.public_send(:secret_method) # Raises NoMethodError
2. Override send in Your Class
You can override send in your class to restrict private method access.
class SecureDemo
def send(method, *args)
if private_methods.include?(method)
raise NoMethodError, "Attempt to call private method `#{method}`"
else
super
end
end
private
def secret_method
"This is private!"
end
end
d = SecureDemo.new
puts d.send(:secret_method) # Raises NoMethodError
Use private_method_defined? to Manually Check for Access Level
Before calling a method via send, verify if it is public.
class SecureDemo
def send(method, *args)
if self.class.private_method_defined?(method)
raise NoMethodError, "private method `#{method}` called"
else
super
end
end
private
def secret_method
"This is private!"
end
end
d = SecureDemo.new
puts d.send(:secret_method) # Raises NoMethodError
Why private_method_defined? Instead of method_defined??
method_defined? checks for public and protected methods but ignores private ones.
private_method_defined? explicitly checks for private methods, which is what we need here.
This ensures only public methods are called dynamically.
puts, p, and print – What’s the Difference?
Ruby provides multiple ways to output text:
puts calls .to_s on its argument and prints it, followed by a newline, but always returns nil.
p prints the argument using .inspect, preserving its original representation and also returns the argument.
Using a linter and following a style guide ensures consistency and readability. Tools like RuboCop help enforce best practices.
These features showcase Rubyโs power and expressiveness. With its readable syntax, metaprogramming capabilities, and intuitive design, Ruby remains a top choice for developers who value simplicity and elegance in their code.