Ruby Blocks and Modules
Blocks and Modules are two important language features in Ruby. Blocks provide code reuse and callback mechanisms, while Modules achieve code sharing and namespace management. This chapter will introduce in detail the concepts, usage, and best practices of blocks and modules in Ruby.
📦 Blocks
What is a Block?
A Block is a special syntax structure in Ruby, consisting of a piece of code that can be passed to methods. Blocks are not objects, but can be called and executed by methods.
# Define single-line block using curly braces
[1, 2, 3].each { |n| puts n }
# Define multi-line block using do...end
[1, 2, 3].each do |n|
puts "Number: #{n}"
endBlock Parameters
# Block without parameters
3.times { puts "Hello" }
# Single parameter block
[1, 2, 3].each { |n| puts n }
# Multi-parameter block
{a: 1, b: 2}.each { |key, value| puts "#{key}: #{value}" }
# Parameter destructuring
coordinates = [[1, 2], [3, 4], [5, 6]]
coordinates.each { |(x, y)| puts "Coordinates: (#{x}, #{y})" }yield Statement
# Use yield in method to call block
def my_method
puts "Method start"
yield if block_given?
puts "Method end"
end
my_method { puts "Code in block" }
# Output:
# Method start
# Code in block
# Method end
# yield with parameters
def calculate(a, b)
result = yield(a, b) if block_given?
puts "Calculation result: #{result}"
end
calculate(10, 5) { |x, y| x + y } # Calculation result: 15
calculate(10, 5) { |x, y| x * y } # Calculation result: 50Block Return Values
# Blocks have return values
def with_calculation
result = yield if block_given?
puts "Block returned: #{result}"
end
with_calculation { 2 + 3 } # Block returned: 5
# Call block multiple times
def repeat(n)
results = []
n.times do |i|
results << yield(i) if block_given?
end
results
end
results = repeat(3) { |i| i * i }
puts results.inspect # [0, 1, 4]Block Local Variables
# Block local variables
total = 0
[1, 2, 3].each do |n; block_local|
block_local = n * 2
total += block_local
puts "Local variable: #{block_local}, Total: #{total}"
end
# Block parameters with same name as external variables
x = 10
[1, 2, 3].each do |x|
puts "Block parameter x: #{x}" # 1, 2, 3
end
puts "External variable x: #{x}" # 10🔧 Blocks as Parameters
&block Parameter
# Explicitly accept block parameter
def explicit_block(&block)
if block_given?
puts "Block exists"
block.call # Call block
else
puts "No block"
end
end
explicit_block { puts "Hello from block" }
explicit_block
# Pass block to other methods
def pass_block_to_other_method(&block)
[1, 2, 3].map(&block) if block
end
result = pass_block_to_other_method { |n| n * 2 }
puts result.inspect # [2, 4, 6]Proc and Lambda
# Create Proc object
my_proc = Proc.new { |x| x * 2 }
puts my_proc.call(5) # 10
# Create Lambda object
my_lambda = lambda { |x| x * 2 }
puts my_lambda.call(5) # 10
# Shorthand Lambda syntax
my_lambda2 = ->(x) { x * 2 }
puts my_lambda2.call(5) # 10
# Difference between Proc and Lambda
def test_return
proc = Proc.new { return "Proc returns" }
lambda_obj = lambda { return "Lambda returns" }
proc_result = proc.call
lambda_result = lambda_obj.call
puts "Proc result: #{proc_result}"
puts "Lambda result: #{lambda_result}"
"Method ended"
end
# test_return will return "Proc returns" instead of "Method ended"Block Conversion
# Convert symbols to blocks
numbers = [1, 2, 3, 4, 5]
squared = numbers.map(&:square) # Equivalent to numbers.map { |n| n.square }
# Custom method supporting symbol-to-block conversion
class Integer
def square
self * self
end
end
numbers = [1, 2, 3, 4, 5]
squared = numbers.map(&:square)
puts squared.inspect # [1, 4, 9, 16, 25]
# Convert Proc to block
my_proc = Proc.new { |n| n * 2 }
result = [1, 2, 3].map(&my_proc)
puts result.inspect # [2, 4, 6]🎯 Modules
What is a Module?
A Module is a container for organizing code in Ruby, containing methods, constants, and classes. Modules cannot be instantiated but can be included in classes or used as namespaces.
# Define module
module MathUtils
PI = 3.14159
def self.square(n)
n * n
end
def cube(n)
n * n * n
end
end
# Use module methods
puts MathUtils::PI # 3.14159
puts MathUtils.square(5) # 25Modules as Namespaces
# Use modules to organize related functionality
module Database
class Connection
def initialize(host, port)
@host = host
@port = port
end
end
class Query
def initialize(sql)
@sql = sql
end
end
def self.connect(host, port)
Connection.new(host, port)
end
end
# Use namespaces
connection = Database::Connection.new("localhost", 5432)
query = Database::Query.new("SELECT * FROM users")
db_connection = Database.connect("localhost", 5432)Mixin
# Define mixable module
module Comparable
def >(other)
compareTo(other) > 0
end
def <(other)
compareTo(other) < 0
end
def ==(other)
compareTo(other) == 0
end
end
# Define class supporting comparison
class Person
include Comparable # Mix in module
attr_reader :name, :age
def initialize(name, age)
@name = name
@age = age
end
def compareTo(other)
@age <=> other.age
end
def to_s
"#{@name}(#{@age} years old)"
end
end
# Use mixed-in functionality
person1 = Person.new("Zhang San", 25)
person2 = Person.new("Li Si", 30)
puts person1 < person2 # true
puts person1 > person2 # false
puts person1 == person2 # falseextend and include Differences
module MyModule
def instance_method
"Instance Method"
end
def self.class_method
"Class Method"
end
end
class MyClass
include MyModule # Include module, add instance methods
end
class AnotherClass
extend MyModule # Extend module, add class methods
end
obj = MyClass.new
puts obj.instance_method # Instance Method
# puts obj.class_method # Error
puts AnotherClass.instance_method # Instance Method (as class method)
# puts AnotherClass.class_method # ErrorModule Methods
# Different ways to define module methods
module MyModule
# Method 1: Use def self.method_name
def self.module_method1
"Module Method 1"
end
# Method 2: Use module_function
def utility_method
"Utility Method"
end
module_function :utility_method
# Method 3: Define class methods within module
class << self
def module_method2
"Module Method 2"
end
end
end
# Use module methods
puts MyModule.module_method1 # Module Method 1
puts MyModule.utility_method # Utility Method
puts MyModule.module_method2 # Module Method 2
# Can also use module_function defined methods after mixing in
class MyClass
include MyModule
end
obj = MyClass.new
puts obj.utility_method # Utility Method🔄 Module Advanced Features
Module Include Chain
module A
def method_a
"Method from A"
end
end
module B
def method_b
"Method from B"
end
end
class MyClass
include A
include B
end
obj = MyClass.new
puts obj.method_a # Method from A
puts obj.method_b # Method from B
# View include chain
puts MyClass.ancestors.inspect
# [MyClass, B, A, Object, Kernel, BasicObject]Module Prepending
# Use prepend to change method lookup order
module Logging
def greet
puts "Start logging"
result = super # Call original method
puts "Logging complete"
result
end
end
class Person
prepend Logging # Prepend module
def greet
"Hello!"
end
end
person = Person.new
puts person.greet
# Output:
# Start logging
# Hello!
# Logging completeModule Constants
module Constants
PI = 3.14159
VERSION = "1.0.0"
class Configuration
DEFAULT_TIMEOUT = 30
end
end
# Access module constants
puts Constants::PI # 3.14159
puts Constants::VERSION # 1.0.0
puts Constants::Configuration::DEFAULT_TIMEOUT # 30
# Constant lookup
module Outer
CONST = "Outer Constant"
module Inner
CONST = "Inner Constant"
def self.show_constants
puts CONST # Inner Constant
puts ::Outer::CONST # Outer Constant
end
end
end
Outer::Inner.show_constants🧪 Block and Module Practice Examples
DSL (Domain-Specific Language) Builder
module DSLBuilder
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def build(&block)
instance = new
instance.instance_eval(&block) if block_given?
instance
end
end
def method_missing(method_name, *args, &block)
if method_name.to_s.end_with?('=')
# setter method
instance_variable_set("@#{method_name.to_s.chomp('=')}", args.first)
else
# getter method
instance_variable_get("@#{method_name}")
end
end
def respond_to_missing?(method_name, include_private = false)
true
end
end
class Person
include DSLBuilder
def initialize
@name = nil
@age = nil
@email = nil
end
def to_s
"Name: #{@name}, Age: #{@age}, Email: #{@email}"
end
end
# Use DSL builder
person = Person.build do
name = "Zhang San"
age = 25
email = "zhangsan@example.com"
end
puts person # Name: Zhang San, Age: 25, Email: zhangsan@example.comConfiguration Management Module
module Configurable
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def config(&block)
@config ||= {}
if block_given?
block.call(@config)
else
@config
end
end
end
def config
self.class.config
end
end
class Application
include Configurable
config do |c|
c[:database_url] = "postgresql://localhost/myapp"
c[:port] = 3000
c[:debug] = true
end
def self.start
puts "Starting application..."
puts "Database: #{config[:database_url]}"
puts "Port: #{config[:port]}"
puts "Debug mode: #{config[:debug] ? 'enabled' : 'disabled'}"
end
end
Application.start
# Output:
# Starting application...
# Database: postgresql://localhost/myapp
# Port: 3000
# Debug mode: enabledTest Framework Simulation
module TestFramework
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def test(name, &block)
@tests ||= []
@tests << {name: name, block: block}
end
def run_tests
@tests ||= []
puts "Running #{@tests.length} tests..."
@tests.each do |test|
print "Test '#{test[:name]}': "
begin
test[:block].call
puts "Passed"
rescue => e
puts "Failed - #{e.message}"
end
end
end
end
end
class MyTests
include TestFramework
test "Addition test" do
result = 2 + 3
raise "Expected 5, got #{result}" unless result == 5
end
test "String test" do
str = "hello"
raise "Expected length 5, got #{str.length}" unless str.length == 5
end
test "Failed test" do
raise "This is an intentional failure"
end
end
MyTests.run_tests🎯 Block and Module Best Practices
1. Block Usage Best Practices
# Good practice: Provide useful block parameters
def with_timer
start_time = Time.now
result = yield(start_time) if block_given?
end_time = Time.now
puts "Execution time: #{end_time - start_time} seconds"
result
end
with_timer { |start| sleep(1); "Completed at #{start}" }
# Good practice: Check if block exists
def optional_block
puts "Start processing"
yield if block_given?
puts "Processing complete"
end
optional_block # Works without passing block
optional_block { puts "Block processing" }
# Avoid: Repeatedly calling block_given? in loop
# Not recommended:
def bad_example
1000.times do |i|
yield(i) if block_given? # Check every time
end
end
# Recommended:
def good_example(&block)
return unless block
1000.times do |i|
block.call(i)
end
end2. Module Design Best Practices
# Good practice: Single responsibility modules
module Authentication
def authenticate(username, password)
# Authentication logic
end
end
module Authorization
def authorize(user, action)
# Authorization logic
end
end
class UserController
include Authentication
include Authorization
end
# Good practice: Use modules to organize related constants
module HttpStatus
OK = 200
NOT_FOUND = 404
SERVER_ERROR = 500
def self.message(code)
case code
when OK then "OK"
when NOT_FOUND then "Not Found"
when SERVER_ERROR then "Internal Server Error"
else "Unknown"
end
end
end
puts HttpStatus.message(HttpStatus.OK) # OK3. Mixin Best Practices
# Good practice: Define clear interfaces
module Timestampable
def created_at
@created_at ||= Time.now
end
def updated_at
@updated_at ||= Time.now
end
def touch
@updated_at = Time.now
end
end
class Post
include Timestampable
def initialize(title)
@title = title
end
end
post = Post.new("My Article")
puts post.created_at
post.touch
puts post.updated_at4. Namespace Best Practices
# Good practice: Use nested modules to organize large projects
module MyApp
module Models
class User
# User model
end
class Post
# Post model
end
end
module Controllers
class UsersController
# User controller
end
class PostsController
# Post controller
end
end
module Services
class EmailService
# Email service
end
class PaymentService
# Payment service
end
end
end
# Use full namespace
user = MyApp::Models::User.new
controller = MyApp::Controllers::UsersController.new📚 Next Steps
After mastering Ruby blocks and modules, it is recommended to continue learning:
- Ruby Classes and Objects - Deep dive into object-oriented programming
- Ruby Inheritance and Polymorphism - Master object-oriented advanced features
- Ruby Metaprogramming - Learn dynamic code generation
- Ruby Design Patterns - Learn common design pattern applications
Continue your Ruby learning journey!