Functional Programming Patterns in Ruby

Ruby is object-oriented. But it handles functional programming well too. You can write cleaner code by mixing both styles.

What Makes Code Functional

Functional programming avoids changing state. Functions take inputs and return outputs. They do not modify external variables.

Three core ideas:

  1. Pure functions - Same input always gives same output
  2. Immutability - Data does not change after creation
  3. First-class functions - Pass functions around like any other value

Ruby's Functional Tools

Ruby has everything you need for functional programming.

Lambdas and Procs

Store functions in variables. Pass them to methods.

double = ->(x) { x * 2 }
triple = ->(x) { x * 3 }

[1, 2, 3].map(&double)  # => [2, 4, 6]
[1, 2, 3].map(&triple)  # => [3, 6, 9]

Map, Select, Reduce

These methods transform data without mutating the original.

numbers = [1, 2, 3, 4, 5]

# Transform each element
squares = numbers.map { |n| n ** 2 }
# => [1, 4, 9, 16, 25]

# Filter elements
evens = numbers.select { |n| n.even? }
# => [2, 4]

# Accumulate into single value
sum = numbers.reduce(0) { |acc, n| acc + n }
# => 15

Original numbers array stays unchanged.

Method Chaining

Chain transformations together. Each step returns a new collection.

users = [
  { name: 'Alice', age: 25, active: true },
  { name: 'Bob', age: 30, active: false },
  { name: 'Carol', age: 28, active: true }
]

active_names = users
  .select { |u| u[:active] }
  .map { |u| u[:name] }
  .sort
# => ['Alice', 'Carol']

Pure Functions in Practice

A pure function has no side effects. It does not modify arguments or external state.

# Impure - modifies the input array
def add_item_bad(items, item)
  items << item
end

# Pure - returns new array
def add_item_good(items, item)
  items + [item]
end

original = [1, 2, 3]
add_item_bad(original, 4)
# original is now [1, 2, 3, 4] - mutated

original = [1, 2, 3]
result = add_item_good(original, 4)
# original is still [1, 2, 3]
# result is [1, 2, 3, 4]

Function Composition

Combine small functions into bigger ones.

add_one = ->(x) { x + 1 }
double = ->(x) { x * 2 }
square = ->(x) { x ** 2 }

# Compose manually
composed = ->(x) { square.call(double.call(add_one.call(x))) }
composed.call(3)  # => 64 (3+1=4, 4*2=8, 8**2=64)

# Or use then (Ruby 2.6+)
result = 3.then(&add_one).then(&double).then(&square)
# => 64

Freezing Objects

Use freeze to prevent mutations.

config = {
  api_url: 'https://api.example.com',
  timeout: 30
}.freeze

config[:timeout] = 60  # Raises FrozenError

When to Use Functional Patterns

Good fit:

  • Data transformations
  • Processing collections
  • Building pipelines
  • Calculations with no side effects

Bad fit:

  • I/O operations
  • Database writes
  • State machines

Mix both styles. Use functional patterns where they make code clearer. Ruby lets you choose.