Add Many monad to Active Support and Active Record Relation in Rails

Rails added the Many monad to Active Support and Active Record Relation to simplify deep chaining on collections. This feature makes it easier to work with collections and perform operations that would otherwise require complex nested iterations.

What is the Many Monad?

The Many monad is a functional programming pattern that provides a clean way to chain operations on collections. It allows you to perform operations on collections without explicitly iterating through them.

Basic Usage

With Arrays

Use Many with arrays to chain operations:

# Before: Complex nested operations
users.map { |user| user.posts.map(&:title) }.flatten

# With Many monad
users.flat_map { |user| user.posts.map(&:title) }

# Or using Many
Many(users).flat_map { |user| user.posts.map(&:title) }

With Active Record Relations

Use Many with Active Record relations:

# Get all comments from all posts by active users
Many(User.active).flat_map(&:posts).flat_map(&:comments)

Common Operations

Flat Map

Flatten nested collections:

users = [user1, user2, user3]

# Get all post titles from all users
Many(users).flat_map { |user| user.posts.map(&:title) }

Map

Transform each element:

# Get all user emails
Many(users).map(&:email)

Filter

Filter elements:

# Get only active users
Many(users).filter(&:active?)

Reduce

Accumulate values:

# Sum all post counts
Many(users).reduce(0) { |sum, user| sum + user.posts.count }

Active Record Integration

Chaining Relations

Chain Active Record relations easily:

# Get all tags from posts by active users
Many(User.active).flat_map(&:posts).flat_map(&:tags).uniq

Combining with Queries

Combine with Active Record queries:

# Get all comments from published posts
Many(Post.published).flat_map(&:comments).where(approved: true)

Real-World Examples

Example 1: Collecting All Tags

# Get all unique tags from all posts
tags = Many(User.active)
  .flat_map(&:posts)
  .flat_map(&:tags)
  .uniq

Example 2: Finding Related Content

# Get all related posts through shared tags
user_posts = user.posts
related_posts = Many(user_posts)
  .flat_map(&:tags)
  .flat_map(&:posts)
  .reject { |post| user_posts.include?(post) }
  .uniq

Example 3: Aggregating Data

# Get total likes across all posts
total_likes = Many(User.active)
  .flat_map(&:posts)
  .sum(&:likes_count)

Benefits

The Many monad provides several advantages:

  • Cleaner code: Reduces nested iterations
  • Readability: More declarative than imperative loops
  • Composability: Easy to chain operations
  • Performance: Can be optimized internally

Comparison with Traditional Approaches

Before Many Monad

# Complex nested iteration
all_tags = []
User.active.each do |user|
  user.posts.each do |post|
    post.tags.each do |tag|
      all_tags << tag unless all_tags.include?(tag)
    end
  end
end

With Many Monad

# Clean, declarative approach
all_tags = Many(User.active)
  .flat_map(&:posts)
  .flat_map(&:tags)
  .uniq

Advanced Usage

Custom Operations

You can combine Many with custom operations:

# Get all post titles with their author names
Many(User.active)
  .flat_map do |user|
    user.posts.map { |post| "#{user.name}: #{post.title}" }
  end

Conditional Chaining

Chain conditionally:

# Get comments from posts, but only if user is active
Many(User.active)
  .flat_map(&:posts)
  .select { |post| post.published? }
  .flat_map(&:comments)

Performance Considerations

While Many provides cleaner code, be mindful of performance:

# This loads all users and posts into memory
Many(User.all).flat_map(&:posts)

# Better: Use database queries when possible
Post.joins(:user).where(users: { active: true })

Best Practices

  1. Use for in-memory collections: Many works best with already-loaded collections
  2. Combine with queries: Use Active Record queries when possible for better performance
  3. Keep it readable: Don't over-chain operations
  4. Consider N+1 queries: Be aware of potential N+1 query issues

Conclusion

The Many monad in Rails provides a clean, functional approach to working with collections. It simplifies deep chaining operations and makes code more readable. While it's great for in-memory operations, remember to use Active Record queries when performance is critical.

Add Many monad to Active Support and Active Record Relation in Rails - Abhay Nikam