Overview
Monads and functors represent abstractions from category theory adapted for functional programming. These patterns encapsulate values within computational contexts and define operations for transforming and combining those contexts. A functor maps between categories while preserving structure, implementing a transform operation that applies functions to wrapped values. A monad extends functor capabilities by adding operations that flatten nested contexts and sequence computations that produce wrapped values.
The practical significance emerges when managing effects like optional values, error handling, asynchronous operations, or state manipulation. Rather than unwrapping values, checking conditions, and manually rewrapping results at each step, these abstractions handle the plumbing automatically. Code becomes declarative, describing transformations rather than control flow mechanics.
Ruby's dynamic nature and flexible syntax enable monad and functor implementations, though the language lacks native support found in strictly functional languages. The community has developed several libraries implementing these patterns, while Ruby's built-in Enumerable module demonstrates functor-like behavior through methods like map and flat_map.
# Basic functor behavior - transforming wrapped values
[1, 2, 3].map { |x| x * 2 }
# => [6, 4, 6]
# Monad-like behavior - flattening nested structures
[[1, 2], [3, 4]].flat_map { |arr| arr.map { |x| x * 2 } }
# => [2, 4, 6, 8]
The terminology derives from abstract algebra: functor from function-like mappings between categories, monad from monoid structures in category theory. Understanding these patterns requires distinguishing the wrapped value from its container, recognizing that operations work on the computational context rather than directly on contained values.
Key Principles
A functor consists of a container type and a mapping function. The container wraps values of some type, while the mapping function transforms wrapped values by applying a provided function without unwrapping. The transformation preserves structure - mapping over an empty container yields an empty container, mapping over a container with three elements yields a container with three transformed elements.
Functors satisfy two laws. The identity law states that mapping with the identity function returns the original container unchanged. The composition law requires that mapping with composed functions produces the same result as mapping with each function sequentially. These laws ensure predictable behavior and enable equational reasoning about code.
# Identity law: fmap(identity) = identity
class Box
attr_reader :value
def initialize(value)
@value = value
end
def fmap(&block)
Box.new(block.call(@value))
end
end
identity = ->(x) { x }
box = Box.new(42)
box.fmap(&identity).value == box.value
# => true
# Composition law: fmap(f . g) = fmap(f) . fmap(g)
double = ->(x) { x * 2 }
add_one = ->(x) { x + 1 }
box.fmap { |x| add_one.call(double.call(x)) }.value ==
box.fmap(&double).fmap(&add_one).value
# => true
Monads extend functors with two additional operations: unit (also called return or pure) and bind (also called flatMap or chain). Unit wraps a bare value into the monadic context. Bind applies a function that returns a monad to a monadic value, automatically flattening the result to avoid nested monads.
The monad laws constrain these operations. Left identity requires that binding over a unit-wrapped value applies the function directly. Right identity states that binding with unit returns the original monad unchanged. Associativity ensures that the order of binding operations doesn't matter - binding functions f and g sequentially produces the same result as binding a function that binds f then g.
# Monad operations: unit and bind
class Maybe
def self.unit(value)
value.nil? ? Nothing.new : Just.new(value)
end
def bind(&block)
raise NotImplementedError
end
end
class Just < Maybe
attr_reader :value
def initialize(value)
@value = value
end
def bind(&block)
block.call(@value)
end
end
class Nothing < Maybe
def bind(&block)
self
end
end
# Left identity: unit(a).bind(f) = f(a)
f = ->(x) { Maybe.unit(x * 2) }
Maybe.unit(21).bind(&f).value == f.call(21).value
# => true
# Right identity: m.bind(unit) = m
m = Maybe.unit(42)
m.bind { |x| Maybe.unit(x) }.value == m.value
# => true
The distinction between map (functor operation) and bind (monad operation) centers on return types. Map takes a function from a bare type to another bare type and lifts it to work on wrapped values. Bind takes a function from a bare type to a wrapped type, preventing nested wrappers by flattening automatically.
Applicative functors occupy the middle ground between functors and monads. They extend functors with operations for applying wrapped functions to wrapped values, enabling multi-argument functions to work on wrapped arguments. Every monad qualifies as an applicative functor, which qualifies as a functor, forming a hierarchy of abstractions with increasing power and constraints.
Ruby Implementation
Ruby lacks first-class support for algebraic data types and type classes, requiring explicit implementation of monad and functor patterns. The dynamic type system permits flexible implementations but sacrifices compile-time guarantees provided by statically typed functional languages.
The Maybe monad handles optional values without explicit nil checks. The Just variant wraps present values, while Nothing represents absence. Operations on Nothing short-circuit without executing the provided function, propagating absence through a computation chain.
class Maybe
def self.unit(value)
value.nil? ? Nothing.new : Just.new(value)
end
def self.from_nullable(value)
unit(value)
end
def fmap(&block)
bind { |value| Maybe.unit(block.call(value)) }
end
end
class Just < Maybe
attr_reader :value
def initialize(value)
@value = value
end
def bind(&block)
block.call(@value)
end
def value_or(_default)
@value
end
def to_s
"Just(#{@value})"
end
end
class Nothing < Maybe
def bind(&block)
self
end
def value_or(default)
default
end
def to_s
"Nothing"
end
end
# Chaining operations without nil checks
def find_user(id)
# Simulated database lookup
id == 1 ? Maybe.unit({id: 1, name: "Alice", address_id: 10}) : Maybe.unit(nil)
end
def find_address(address_id)
address_id == 10 ? Maybe.unit({id: 10, street: "Main St"}) : Maybe.unit(nil)
end
result = find_user(1)
.bind { |user| Maybe.unit(user[:address_id]) }
.bind { |addr_id| find_address(addr_id) }
.bind { |addr| Maybe.unit(addr[:street]) }
puts result.value_or("Unknown address")
# => Main St
The Either monad extends Maybe by capturing failure reasons. The Right variant represents success with a value, while Left represents failure with an error. By convention, computations proceed through Right values and short-circuit on Left, accumulating the first error encountered.
class Either
def self.right(value)
Right.new(value)
end
def self.left(error)
Left.new(error)
end
def fmap(&block)
bind { |value| Either.right(block.call(value)) }
end
end
class Right < Either
attr_reader :value
def initialize(value)
@value = value
end
def bind(&block)
block.call(@value)
end
def value_or(_default)
@value
end
def either(left_fn, right_fn)
right_fn.call(@value)
end
end
class Left < Either
attr_reader :error
def initialize(error)
@error = error
end
def bind(&block)
self
end
def value_or(default)
default
end
def either(left_fn, right_fn)
left_fn.call(@error)
end
end
# Error handling without exceptions
def parse_integer(str)
Integer(str)
Either.right(Integer(str))
rescue ArgumentError
Either.left("Invalid integer: #{str}")
end
def divide(numerator, denominator)
denominator.zero? ?
Either.left("Division by zero") :
Either.right(numerator / denominator)
end
result = parse_integer("10")
.bind { |n| parse_integer("2").fmap { |d| [n, d] } }
.bind { |n, d| divide(n, d) }
result.either(
->(error) { puts "Error: #{error}" },
->(value) { puts "Result: #{value}" }
)
# => Result: 5
The List monad treats arrays as contexts for non-deterministic computation. Binding a function over a list applies that function to each element and concatenates the results, naturally expressing computations that branch into multiple possibilities.
class List
attr_reader :items
def self.unit(value)
List.new([value])
end
def initialize(items)
@items = items
end
def bind(&block)
result = @items.flat_map { |item| block.call(item).items }
List.new(result)
end
def fmap(&block)
List.new(@items.map(&block))
end
end
# Non-deterministic computation
def pairs(n)
List.new((1..n).to_a).bind { |x|
List.new((1..n).to_a).bind { |y|
List.unit([x, y])
}
}
end
# Pythagorean triples
def pythagorean_triples(n)
List.new((1..n).to_a).bind { |x|
List.new((x..n).to_a).bind { |y|
List.new((y..n).to_a).bind { |z|
x * x + y * y == z * z ?
List.unit([x, y, z]) :
List.new([])
}
}
}
end
puts pythagorean_triples(15).items.inspect
# => [[3, 4, 5], [5, 12, 13], [6, 8, 10], [9, 12, 15]]
Ruby's Enumerable module demonstrates functor properties through map and monad properties through flat_map, though it doesn't enforce functor laws or provide the full monad interface. Custom implementations gain explicit control over behavior and can implement additional operations.
# Using Ruby's built-in monad-like operations
result = [1, 2, 3]
.flat_map { |x| [x, x * 10] }
.map { |x| x + 1 }
.select { |x| x > 5 }
puts result.inspect
# => [6, 11, 21, 31]
The dry-monads gem provides production-ready monad implementations with additional features like result type conversion, do-notation through blocks, and integration with dry-validation. This library offers the most mature Ruby monad implementation for production applications.
require 'dry/monads'
class UserService
include Dry::Monads[:result, :maybe]
def find_user(id)
user = database_lookup(id)
user ? Success(user) : Failure(:user_not_found)
end
def validate_age(user)
user[:age] >= 18 ?
Success(user) :
Failure(:user_too_young)
end
def process(id)
find_user(id)
.bind { |user| validate_age(user) }
.fmap { |user| user[:name] }
end
end
Common Patterns
The railway-oriented programming pattern uses Either monads to model computation tracks. Successful operations stay on the "success track" (Right), while failures switch to the "error track" (Left). Once on the error track, subsequent operations bypass until reaching explicit error handling.
class Result
def self.success(value)
Success.new(value)
end
def self.failure(error)
Failure.new(error)
end
def self.from_exception
begin
Result.success(yield)
rescue StandardError => e
Result.failure(e.message)
end
end
end
class Success < Result
attr_reader :value
def initialize(value)
@value = value
end
def bind(&block)
block.call(@value)
end
def success?
true
end
end
class Failure < Result
attr_reader :error
def initialize(error)
@error = error
end
def bind(&block)
self
end
def success?
false
end
end
# Processing pipeline with error handling
def validate_email(email)
email.include?('@') ?
Result.success(email) :
Result.failure("Invalid email format")
end
def check_domain(email)
domain = email.split('@').last
%w[gmail.com yahoo.com].include?(domain) ?
Result.success(email) :
Result.failure("Domain not allowed")
end
def create_user(email)
Result.from_exception do
# Simulated user creation
{email: email, id: rand(1000)}
end
end
result = validate_email("user@gmail.com")
.bind { |e| check_domain(e) }
.bind { |e| create_user(e) }
if result.success?
puts "User created: #{result.value}"
else
puts "Failed: #{result.error}"
end
The Maybe monad chain pattern sequences operations where each step might fail. Rather than checking for nil after each operation, the chain automatically short-circuits on Nothing and continues with Just values. This eliminates nested conditionals and makes the happy path explicit.
class User
attr_reader :id, :profile
def initialize(id, profile)
@id = id
@profile = profile
end
end
class Profile
attr_reader :settings
def initialize(settings)
@settings = settings
end
end
def find_user_setting(user_id, setting_key)
Maybe.from_nullable(fetch_user(user_id))
.bind { |user| Maybe.from_nullable(user.profile) }
.bind { |profile| Maybe.from_nullable(profile.settings) }
.bind { |settings| Maybe.from_nullable(settings[setting_key]) }
.value_or("default_value")
end
def fetch_user(id)
return nil if id > 100
User.new(id, Profile.new({theme: "dark", language: "en"}))
end
puts find_user_setting(50, :theme)
# => dark
puts find_user_setting(150, :theme)
# => default_value
The list monad pattern generates combinations or searches solution spaces. Each bind operation branches computation into multiple paths, with flat_map flattening the nested results into a single sequence. This pattern naturally expresses problems involving permutations, combinations, or constraint satisfaction.
# Generating combinations
def chess_positions
files = ('a'..'h').to_a
ranks = (1..8).to_a
List.new(files).bind { |file|
List.new(ranks).bind { |rank|
List.unit("#{file}#{rank}")
}
}
end
# Finding solutions with constraints
def valid_queens(n)
def safe?(positions, row, col)
positions.each_with_index.all? { |c, r|
c != col && (row - r).abs != (col - c).abs
}
end
def place_queens(positions, row, n)
return List.unit(positions) if row == n
List.new((0...n).to_a).bind { |col|
safe?(positions, row, col) ?
place_queens(positions + [col], row + 1, n) :
List.new([])
}
end
place_queens([], 0, n)
end
solutions = valid_queens(4)
puts "Found #{solutions.items.length} solutions"
# => Found 2 solutions
The kleisli composition pattern combines monadic functions by composing their effects. Given functions a → M b and b → M c, kleisli composition produces a → M c, sequencing the monadic effects without manual chaining. This enables building complex operations from simple monadic functions.
module Kleisli
def self.compose(f, g)
->(x) { f.call(x).bind(&g) }
end
end
# Individual monadic operations
parse_int = ->(s) {
Integer(s)
Maybe.unit(Integer(s))
rescue ArgumentError
Maybe.unit(nil)
}
double = ->(n) { Maybe.unit(n * 2) }
increment = ->(n) { Maybe.unit(n + 1) }
# Composed operation
process = Kleisli.compose(
parse_int,
Kleisli.compose(double, increment)
)
puts process.call("5").value_or(0)
# => 11
puts process.call("invalid").value_or(0)
# => 0
Practical Examples
A configuration loader demonstrates Maybe monad usage for handling missing or invalid configuration values. Each step in loading configuration might fail - file might not exist, JSON might be invalid, required keys might be missing. The Maybe monad chain makes the sequence explicit without cluttering code with nil checks.
require 'json'
class ConfigLoader
def self.load(path, *keys)
read_file(path)
.bind { |content| parse_json(content) }
.bind { |config| extract_keys(config, keys) }
end
def self.read_file(path)
Maybe.unit(File.read(path))
rescue Errno::ENOENT, IOError
Maybe.unit(nil)
end
def self.parse_json(content)
Maybe.unit(JSON.parse(content))
rescue JSON::ParserError
Maybe.unit(nil)
end
def self.extract_keys(config, keys)
result = keys.reduce(config) { |obj, key|
return nil unless obj.is_a?(Hash) && obj.key?(key.to_s)
obj[key.to_s]
}
Maybe.unit(result)
end
end
# Usage
database_url = ConfigLoader
.load("config/database.yml", "production", "url")
.value_or("sqlite://memory")
api_key = ConfigLoader
.load("config/secrets.yml", "api", "key")
.value_or(ENV["API_KEY"])
A validation pipeline shows Either monad application for accumulating validation errors while processing input. Unlike Maybe which loses failure information, Either captures why validation failed and can chain multiple validators that each might produce different errors.
class Validator
def self.validate_presence(value, field)
value.nil? || value.empty? ?
Either.left("#{field} is required") :
Either.right(value)
end
def self.validate_length(value, field, min:, max:)
length = value.length
if length < min
Either.left("#{field} must be at least #{min} characters")
elsif length > max
Either.left("#{field} must be at most #{max} characters")
else
Either.right(value)
end
end
def self.validate_format(value, field, pattern:)
pattern.match?(value) ?
Either.right(value) :
Either.left("#{field} has invalid format")
end
end
def validate_username(input)
Validator.validate_presence(input, "Username")
.bind { |v| Validator.validate_length(v, "Username", min: 3, max: 20) }
.bind { |v| Validator.validate_format(v, "Username", pattern: /\A[a-zA-Z0-9_]+\z/) }
end
def validate_email(input)
Validator.validate_presence(input, "Email")
.bind { |v| Validator.validate_format(v, "Email", pattern: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i) }
end
# Process registration
username_result = validate_username("ab")
email_result = validate_email("invalid")
username_result.either(
->(err) { puts "Username error: #{err}" },
->(val) { puts "Valid username: #{val}" }
)
# => Username error: Username must be at least 3 characters
email_result.either(
->(err) { puts "Email error: #{err}" },
->(val) { puts "Valid email: #{val}" }
)
# => Email error: Email has invalid format
A data transformation pipeline uses functor mapping to transform nested data structures while preserving structure. Each transformation step maps over the structure without manually unwrapping and rewrapping values, keeping transformation logic separate from structure traversal.
class Tree
attr_reader :value, :left, :right
def initialize(value, left: nil, right: nil)
@value = value
@left = left
@right = right
end
def fmap(&block)
Tree.new(
block.call(@value),
left: @left&.fmap(&block),
right: @right&.fmap(&block)
)
end
def to_a
result = []
result.concat(@left.to_a) if @left
result << @value
result.concat(@right.to_a) if @right
result
end
end
# Build tree
tree = Tree.new(
5,
left: Tree.new(3, left: Tree.new(1), right: Tree.new(4)),
right: Tree.new(8, left: Tree.new(6), right: Tree.new(9))
)
# Transform all values
doubled = tree.fmap { |x| x * 2 }
incremented = tree.fmap { |x| x + 1 }
stringified = tree.fmap { |x| "Value: #{x}" }
puts doubled.to_a.inspect
# => [2, 6, 8, 10, 12, 16, 18]
puts incremented.to_a.inspect
# => [2, 4, 5, 6, 7, 9, 10]
An async computation example shows how monads model computations with different effects. While Ruby lacks first-class async monads like Haskell's IO monad, the pattern applies to modeling computations that produce values in the future, enabling composition of async operations.
class Async
def initialize(&block)
@block = block
end
def bind(&fn)
Async.new do
result = @block.call
fn.call(result).run
end
end
def fmap(&fn)
bind { |x| Async.unit(fn.call(x)) }
end
def self.unit(value)
Async.new { value }
end
def run
@block.call
end
end
# Simulated async operations
def fetch_user(id)
Async.new do
sleep(0.1) # Simulate network delay
{id: id, name: "User#{id}", posts: [1, 2, 3]}
end
end
def fetch_post(post_id)
Async.new do
sleep(0.1)
{id: post_id, title: "Post #{post_id}", author_id: 1}
end
end
def enrich_user(user_id)
fetch_user(user_id)
.bind { |user|
fetch_posts = user[:posts].map { |pid| fetch_post(pid) }
Async.unit(user.merge(post_details: fetch_posts.map(&:run)))
}
end
result = enrich_user(1).run
puts "User: #{result[:name]}, Posts: #{result[:post_details].length}"
Design Considerations
Monads suit problems where computations produce values within a context that needs consistent handling across multiple steps. Optional values, error conditions, asynchronous operations, state threading, and non-deterministic computation all involve contexts that monads model effectively. The bind operation handles context propagation automatically, letting code focus on transformation logic.
The decision to use monads depends on computation structure. Linear pipelines where each step depends on the previous step's success benefit from monadic composition. Code with multiple independent computations combined at the end fits applicative functors better. Simple data transformations without context need only functor map operations.
Ruby's dynamic typing reduces monad benefits compared to statically typed languages. Type systems in languages like Haskell prevent mixing wrapped and unwrapped values at compile time, catching errors early. Ruby permits calling any method on any object, so wrapping values in monads trades compile-time safety for runtime checking and explicit interfaces.
The verbosity of manual monad implementation in Ruby discourages adoption. Languages with do-notation or comprehension syntax make monadic code read almost like imperative code. Ruby lacks these features, requiring explicit bind calls that can obscure logic. For simple Maybe chains, direct nil checking might be clearer.
# Monadic style - explicit but verbose
def find_nested_value(hash, *keys)
keys.reduce(Maybe.unit(hash)) { |maybe_obj, key|
maybe_obj.bind { |obj|
Maybe.from_nullable(obj.is_a?(Hash) ? obj[key] : nil)
}
}.value_or(nil)
end
# Imperative style - direct but requires nil checks
def find_nested_value_imperative(hash, *keys)
keys.reduce(hash) { |obj, key|
return nil unless obj.is_a?(Hash)
obj[key]
}
end
Library selection impacts monad usability. The dry-monads gem provides mature implementations with syntactic sugar through do-notation blocks and pattern matching integration. Custom implementations offer full control but require more code and testing. Ruby's built-in Enumerable provides functor and monad operations for collections without additional dependencies.
Monads compose sequentially but not horizontally. Combining independent monadic computations requires lifting operations to applicative level or manually extracting values. Monad transformers stack different monadic effects but add complexity. For applications needing multiple effect types simultaneously, alternative patterns like effect systems might fit better.
Performance considerations matter for hot paths. Monadic abstraction introduces object allocation and method call overhead. A tight loop processing millions of items might run faster with direct nil checks than Maybe monad chains. Profile before optimizing, as clarity often matters more than marginal performance gains.
Team familiarity influences adoption success. Developers experienced with functional programming adopt monads easily, while those from imperative backgrounds find the abstraction unfamiliar. Introduce monads gradually in code reviews and documentation rather than converting entire codebases. Focus on pain points like error handling or optional value chains where monads provide clear wins.
Reference
Functor Operations
| Operation | Signature | Description | Laws |
|---|---|---|---|
| fmap | (a -> b) -> F a -> F b | Transform wrapped value | Identity, Composition |
| map | (a -> b) -> F a -> F b | Alias for fmap | Identity, Composition |
Monad Operations
| Operation | Signature | Description | Laws |
|---|---|---|---|
| unit/return/pure | a -> M a | Wrap value in monad | Left identity, Right identity |
| bind/flatMap | M a -> (a -> M b) -> M b | Chain monadic operations | Left identity, Right identity, Associativity |
| join | M (M a) -> M a | Flatten nested monads | Corresponds to bind |
Common Monad Types
| Monad | Purpose | Success Case | Failure Case |
|---|---|---|---|
| Maybe | Optional values | Just(value) | Nothing |
| Either | Error handling | Right(value) | Left(error) |
| List | Non-determinism | Non-empty list | Empty list |
| IO | Side effects | Deferred computation | Not applicable |
| State | State threading | Stateful computation | Not applicable |
| Reader | Environment access | Value with context | Not applicable |
| Writer | Logging/output | Value with log | Not applicable |
Monad Laws
| Law | Formula | Description |
|---|---|---|
| Left Identity | unit(a).bind(f) = f(a) | Wrapping then binding equals direct application |
| Right Identity | m.bind(unit) = m | Binding with unit returns original monad |
| Associativity | m.bind(f).bind(g) = m.bind(x -> f(x).bind(g)) | Binding order doesn't matter |
Functor Laws
| Law | Formula | Description |
|---|---|---|
| Identity | fmap(id) = id | Mapping identity returns original |
| Composition | fmap(f . g) = fmap(f) . fmap(g) | Composed map equals sequential maps |
Ruby Implementation Patterns
| Pattern | Code Structure | Use Case |
|---|---|---|
| Maybe Chain | Maybe.unit(x).bind{}.bind{}.value_or() | Sequential operations with potential failure |
| Either Pipeline | Either.right(x).bind{}.bind{}.either() | Error handling with failure reasons |
| List Comprehension | List.new([]).bind{}.bind{}.items | Non-deterministic computation |
| Railway Pattern | Success(x).bind{}.bind{}.success? | Success/failure track routing |
| Kleisli Composition | Kleisli.compose(f, g) | Combining monadic functions |
Comparison with Imperative Patterns
| Functional Approach | Imperative Equivalent | Trade-off |
|---|---|---|
| Maybe.unit(x).bind(f) | x.nil? ? nil : f(x) | Abstraction vs directness |
| Either.right(x).bind(f) | begin; f(x); rescue; end | Explicit vs exception-based |
| list.fmap(f) | list.map{f} | Generic vs built-in |
| m.bind(f).bind(g) | x = f(m); g(x) | Composition vs sequential |
Common Gotchas
| Issue | Description | Solution |
|---|---|---|
| Nested Monads | bind returns monad, creating M(M a) | Use bind not fmap |
| Wrong Abstraction | Using monad when functor suffices | Check if flattening needed |
| Performance Overhead | Object allocation in tight loops | Profile and optimize hot paths |
| Type Confusion | Mixing wrapped and unwrapped values | Consistent monad usage |
| Imperative Mixing | Breaking chain with non-monadic code | Complete transformation in chain |
Monad Transformer Stack
| Transformer | Base Monad | Result Type | Use Case |
|---|---|---|---|
| MaybeT | Any monad M | M (Maybe a) | Optional values in context |
| EitherT | Any monad M | M (Either e a) | Errors in context |
| StateT | Any monad M | s -> M (a, s) | State with other effects |
| ReaderT | Any monad M | e -> M a | Environment with effects |
Ruby Gems
| Gem | Focus | Key Features | Maturity |
|---|---|---|---|
| dry-monads | General monads | Result, Maybe, Try, List, Task | Production-ready |
| deterministic | Railway programming | Success/Failure, pattern matching | Stable |
| kleisli | Either monad | Right/Left with sugar | Maintained |
| monadic | Pure monads | Academic implementation | Experimental |