← Back to DevBytes

Functional Programming in Ruby

Functional Programming in Ruby: A Comprehensive Guide

Functional programming (FP) is a paradigm that treats computation as the evaluation of mathematical functions and avoids changing state and mutable data. Ruby, though primarily object-oriented, offers robust support for functional programming techniques. This tutorial explores what functional programming means in Ruby, why it matters, how to leverage its power, and best practices to write clean, predictable, and maintainable code.

What is Functional Programming in Ruby?

πŸš€ Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Functional programming centers on pure functions, immutability, higher-order functions, and declarative style. In Ruby, functional programming isn't a strict requirement but a mindset. You can write code that emphasizes:

Ruby’s blocks, Procs, lambdas, and enumerable methods make functional programming natural. You can combine objects with functional purity, gaining the best of both worlds.

Why Functional Programming Matters in Ruby

Embracing FP in Ruby yields several benefits:

Core Functional Concepts in Ruby

Pure Functions

A pure function always produces the same output given the same input and has no side effects (modifying global state, writing to a database, etc.).

# Impure function – relies on external variable and modifies array
counter = 0
def impure_increment
  counter += 1  # side effect: mutates global counter
end

# Pure function
def pure_add(a, b)
  a + b
end
puts pure_add(2, 3) # always 5

Immutability

Ruby objects are mutable by default, but you can adopt an immutable style using frozen objects or simply not mutating data. Use methods that return new instances instead of modifying the receiver.

# Mutable approach (modifies original)
array = [1, 2, 3]
array << 4  # changes original array

# Immutable approach
original = [1, 2, 3]
new_array = original + [4] # original remains [1,2,3]
# Or using freezes
frozen_arr = original.freeze
# frozen_arr cannot be modified

Higher-Order Functions

Ruby supports functions that take blocks, Procs, or lambdas, enabling abstraction of control structures.

def apply_twice(func, value)
  func.call(func.call(value))
end

add_five = ->(x) { x + 5 }
puts apply_twice(add_five, 10) # -> 20

# Using block
def measure(&block)
  start = Time.now
  result = block.call
  elapsed = Time.now - start
  [result, elapsed]
end

Blocks, Procs, and Lambdas

These are the building blocks of functional style. Blocks are implicit anonymous functions, Procs are explicit, and lambdas enforce arity and behave like methods.

# Block
[1, 2, 3].map { |n| n * 2 }

# Proc
times_two = Proc.new { |n| n * 2 }
# or shorthand: times_two = proc { |n| n * 2 }
puts times_two.call(5)  # 10

# Lambda – strict about arguments and returns
greet = ->(name) { "Hello, #{name}" }
puts greet.call("Ruby") # "Hello, Ruby"

# Lambda vs Proc in return behavior
def test_lambda
  l = -> { return "lambda return" }
  l.call
  "after lambda"
end
puts test_lambda # "after lambda"

def test_proc
  p = proc { return "proc return" }
  p.call
  "after proc"
end
puts test_proc # "proc return" (exits method)

How to Use Functional Programming in Ruby

Transform Collections Declaratively

Instead of iterating with each and building results manually, use map, select, reject, reduce.

# Imperative style
numbers = [1, 2, 3, 4, 5]
squared_evens = []
numbers.each do |n|
  if n.even?
    squared_evens << n * n
  end
end

# Functional style
squared_evens = numbers.select(&:even?).map { |n| n * n }
# or using lazy enumeration for large sets
squared_evens = numbers.lazy.select(&:even?).map { |n| n * n }.first(3)

Chain Operations with Method Composition

Use then (or yield_self) to pipeline transformations on single values.

result = "  ruby functional  ".strip.then { |s| s.capitalize }.then { |s| s + "!" }
# result => "Ruby functional!"

Partial Application and Currying

Lambdas can be partially applied to create new functions with some arguments fixed.

multiply = ->(a, b) { a * b }
double = multiply.curry.call(2)
puts double.call(5) # 10

# Using curry for multiple arguments
add = ->(a, b, c) { a + b + c }
add_five_and_ten = add.curry.call(5).call(10)
puts add_five_and_ten.call(3) # 18

Use Functions as Return Values

Create function factories for configurable behavior.

def make_multiplier(factor)
  ->(x) { x * factor }
end

triple = make_multiplier(3)
puts triple.call(7) # 21

Recursion Instead of Loops

Functional style often replaces loops with recursion, though in Ruby you must consider stack depth.

def factorial(n)
  n == 0 ? 1 : n * factorial(n - 1)
end
puts factorial(5) # 120

# Tail-recursive style (Ruby doesn't optimize, but you can use trampolines)
def tail_factorial(n, acc = 1)
  n == 0 ? acc : tail_factorial(n - 1, n * acc)
end

Lazy Enumerators for Infinite Sequences

Combine functional patterns with lazy evaluation to handle potentially infinite data.

fib = Enumerator.new do |y|
  a, b = 0, 1
  loop do
    y << a
    a, b = b, a + b
  end
end

fib.lazy.select(&:even?).first(10) # first 10 even Fibonacci numbers

Value Objects and Data Immutability

Create classes that represent immutable values, using Struct or custom classes with frozen attributes.

class Money
  attr_reader :amount, :currency

  def initialize(amount, currency)
    @amount = amount.freeze
    @currency = currency.freeze
  end

  def add(other)
    if currency == other.currency
      Money.new(amount + other.amount, currency)
    else
      raise "Currency mismatch"
    end
  end
end

Best Practices for Functional Ruby

Conclusion

Functional programming in Ruby is a powerful complement to its object-oriented core. By embracing pure functions, immutability, higher-order functions, and declarative transformations, you can write code that is more predictable, testable, and composable. Ruby’s rich enumerable module, block syntax, and support for closures make it surprisingly capable for functional programming. Start by refactoring small pieces of code to be pure and immutable, gradually incorporating techniques like currying, lazy enumeration, and value objects. Remember, the goal is not to abandon Ruby’s object-oriented nature but to harness the best of both paradigms. With practice, functional programming will become a natural part of your Ruby toolkit, leading to cleaner, more maintainable applications.

πŸš€ Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started β€” $23.99/mo
← Back to all articles