Functional Programming in Ruby — Ruby Deep Dive [03]

Bhavyansh @ DiversePixel
4 min readJul 20, 2024

--

We are talking about Functional Programming in Ruby in this article, covering:

  • Blocks, Procs, Lambdas
  • Higher-Order functions like map, select, reduce
Photo by Mika Baumeister on Unsplash

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:

  1. 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 a return statement is executed inside a Proc, it returns from the method that encloses the Proc.
  • Lambdas: When a return statement is executed inside a lambda, it returns from the lambda 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 and map 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.

--

--

Bhavyansh @ DiversePixel
Bhavyansh @ DiversePixel

Written by Bhavyansh @ DiversePixel

Hey I write about Tech. Join me as I share my tech learnings and insights. 🚀

No responses yet