Functional Programming in Ruby — Ruby Deep Dive [03]
We are talking about Functional Programming in Ruby in this article, covering:
- Blocks, Procs, Lambdas
- Higher-Order functions like map, select, reduce
Blocks in ruby
A block is a piece of code that can be passed into methods as an argument. It’s enclosed within either curly braces {}
or do..end
keywords. Blocks are a powerful feature in Ruby, allowing you to encapsulate behavior and pass it around like an anonymous function or lambda in other programming languages.
def example_method(&block)
puts "Start of method"
block.call if block
puts "End of method"
end
example_method { puts "Inside the block" }
Start of method
Inside the block
End of method
What is a proc?
A Proc
(short for "procedure") in Ruby is an object that encapsulates a block of code, which can be stored in a variable, passed to methods, and executed.
my_proc = proc { puts "Hello from Proc" }
# or
my_proc = Proc.new { puts "Hello from Proc" }
# call as follows
my_proc.call # Output: "Hello from Proc"
And Lambdas?
Procs
and lambdas
are both types of Proc
objects, but they have some differences:
- Argument Handling:
Procs
: Do not enforce the number of arguments strictly.Lambdas
: Enforce the number of arguments strictly.
my_proc = Proc.new { |a, b| puts "a: #{a}, b: #{b}" }
my_proc.call(1, 2) # Output: a: 1, b: 2
my_proc.call(1) # Output: a: 1, b:
my_proc.call(1, 2, 3) # Output: a: 1, b: 2
my_lambda = lambda { |a, b| puts "a: #{a}, b: #{b}" }
my_lambda.call(1, 2) # Output: a: 1, b: 2
my_lambda.call(1) # ArgumentError: wrong number of arguments (given 1, expected 2)
my_lambda.call(1, 2, 3) # ArgumentError: wrong number of arguments (given 3, expected 2)
2. Return Behavior:
Procs
: When areturn
statement is executed inside aProc
, it returns from the method that encloses theProc
.Lambdas
: When areturn
statement is executed inside alambda
, it returns from thelambda
itself.
def example_method
my_proc = Proc.new { return "Returning from Proc" }
result = my_proc.call
return "Returning from method"
end
puts example_method # Output: "Returning from Proc"
def example_method
my_lambda = lambda { return "Returning from lambda" }
result = my_lambda.call
return "Returning from method"
end
puts example_method # Output: "Returning from method"
Proc to a block:
proc_object = Proc.new { puts "Inside the proc" }
example_method(&proc_object) # convert a proc to a block by using &
procs can be passed as arguments:
def example_method(proc_object)
proc_object.call
end
my_proc = Proc.new { puts "Hello from Proc" }
example_method(my_proc) # Output: "Hello from Proc"
Higher-Order Functions
In functional programming, higher-order functions are functions that take other functions as arguments or return them as results. Ruby’s Enumerable module provides several higher-order functions like map
, select
, and reduce
.
map
The map
method transforms each element in a collection according to the block provided.
numbers = [1, 2, 3, 4, 5]
squared_numbers = numbers.map { |n| n ** 2 }
puts squared_numbers # Output: [1, 4, 9, 16, 25]
select
The select
method filters elements in a collection according to the block provided.
numbers = [1, 2, 3, 4, 5]
even_numbers = numbers.select { |n| n.even? }
puts even_numbers # Output: [2, 4]
reduce
The reduce
method reduces a collection to a single value according to the block provided.
numbers = [1, 2, 3, 4, 5]
sum = numbers.reduce(0) { |acc, n| acc + n }
puts sum # Output: 15
Conclusion
In this article, we’ve delved into functional programming concepts in Ruby, including blocks, Procs, lambdas, and higher-order functions. By understanding and utilizing these concepts, you can write more flexible, reusable, and expressive code.
Here is an example that encapsulates all these concepts:
# A class representing a collection of users with methods to manipulate the collection
class UserCollection
attr_accessor :users
def initialize(users = [])
@users = users
end
# Adding a user using a block
def add_user
user = yield
@users << user
end
# Filtering users using a lambda
def filter_users(criteria)
criteria_lambda = lambda { |user| criteria.call(user) }
@users.select(&criteria_lambda)
end
# Transforming users using a proc
def transform_users(proc_object)
@users.map(&proc_object)
end
end
# User class
class User
attr_accessor :name, :age
def initialize(name, age)
@name = name
@age = age
end
def to_s
"#{name}, Age: #{age}"
end
end
# Example usage
users = UserCollection.new
# Adding users using a block
users.add_user { User.new("Alice", 30) }
users.add_user { User.new("Bob", 20) }
users.add_user { User.new("Charlie", 25) }
# Filtering users using a lambda
adult_users = users.filter_users(lambda { |user| user.age >= 21 })
puts "Adult users:"
adult_users.each { |user| puts user }
# Transforming users using a proc
uppercase_names_proc = Proc.new { |user| User.new(user.name.upcase, user.age) }
uppercase_users = users.transform_users(uppercase_names_proc)
puts "\nUsers with uppercase names:"
uppercase_users.each { |user| puts user }
This example showcases:
- Blocks: Adding users using blocks.
- Lambdas: Filtering users based on a criteria lambda.
- Procs: Transforming users using a proc.
- Higher-Order Functions: Using
select
andmap
to filter and transform collections.
By mastering these functional programming concepts in Ruby, you can write more effective and elegant code, leveraging Ruby’s expressive power to its fullest.