Skip to content

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.

ruby
# 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}"
end

Block Parameters

ruby
# 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

ruby
# 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: 50

Block Return Values

ruby
# 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

ruby
# 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

ruby
# 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

ruby
# 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

ruby
# 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.

ruby
# 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) # 25

Modules as Namespaces

ruby
# 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

ruby
# 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 # false

extend and include Differences

ruby
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   # Error

Module Methods

ruby
# 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

ruby
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

ruby
# 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 complete

Module Constants

ruby
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

ruby
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.com

Configuration Management Module

ruby
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: enabled

Test Framework Simulation

ruby
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

ruby
# 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
end

2. Module Design Best Practices

ruby
# 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)  # OK

3. Mixin Best Practices

ruby
# 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_at

4. Namespace Best Practices

ruby
# 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:

Continue your Ruby learning journey!

Content is for learning and research only.