Ruby's filter_map: A Single‑Pass Way to Clean and Transform Collections
Why filter_map Beats Two‑Step Filtering and Mapping
Whenever I need to pull useful data out of a collection, I reach for a combination of select and map. The pattern looks tidy at first, but after a few iterations I notice it creates two passes over the same array and leaves the intent a bit fuzzy. Enter filter_map, a Ruby 2.7+ method that elegantly merges filtering and mapping into one expressive step.
I first started using filter_map on a project where we were compiling a list of active user emails for a weekly digest. We had an array of user hashes, some of which were incomplete or had blank emails. Writing a separate select followed by a map felt redundant, and the intermediate array was a waste of memory. Switching to filter_map simplified the pipeline, made the code more readable, and kept the performance characteristics clean.
The Problem in Plain Terms
Imagine you have:
users = [
{ name: "Alice", email: "alice@example.com" },
{ name: nil, email: nil },
{ name: "Bob", email: "bob@example.com" },
{ name: "Carol", email: "" },
{ name: "Dave", email: "dave@example.com" }
]
You want a clean list of down‑cased email addresses, discarding any entry that lacks a valid email. The classic approach:
valid_emails = users.select { |u| u[:email].present? }
.map { |u| u[:email].downcase }
That works, but it traverses the array twice and creates an intermediate collection of hashes you don’t need later. In larger pipelines, the overhead compounds, and the intent gets buried between two separate calls.
The Solution: One‑Pass filter_map
Ruby’s filter_map is designed exactly for this scenario. It yields each element, and if the block returns nil or falsey, the element is omitted; otherwise, the returned value is added to the resulting array. The effect is a filtered and transformed collection in a single pass.
Use
filter_mapwhen you need to discard unwanted items while simultaneously converting them. It’s the idiomatic way to chainselectandmapwithout the extra iteration.
Here’s the same logic with filter_map:
valid_emails = users.filter_map do |user|
user[:email].presence&&.downcase
end
# => ["alice@example.com", "bob@example.com", "dave@example.com"]
The block returns nil for records without an email, so they are silently dropped. The presence method (from ActiveRecord, but easily reimplemented as user[:email]&.present?) ensures we only proceed when the value exists, and & short‑circuits the call to downcase. The result is a concise, readable pipeline that does exactly what we need in one go.
Why It Matters: Performance and Readability
- Single Pass: The array is traversed only once, reducing CPU overhead, especially noticeable on large datasets.
- No Intermediate Collections: Memory usage stays low because we never allocate an unused intermediate array.
- Clear Intent: The block expresses the transformation directly; you don’t need to chain two separate methods to convey "filter then map".
- Chainability:
filter_mapworks with other enumerables, making it easy to drop into existing pipelines.
Consider a more complex pipeline: you might want to skip users whose accounts are inactive, map to a formatted string, and then sort. With filter_map you can embed the filter logic inside the block, keeping the rest of the chain clean:
active_formatted = users.filter_map do |u|
next nil unless u[:active]
"#{u[:name]} <#{u[:email]}>".downcase
end.sort
Again, we’re filtering, transforming, and sorting in three steps, but the filtering happens inside the block, avoiding a separate select. The code reads like a description of the data flow.
Real‑World Scenario: Generating a Mailing List
At a previous job we built a nightly job that exported contacts for a third‑party CRM. The source was a PostgreSQL table of users, which we pulled into Ruby objects. The export required:
- Discard users without an email.
- Normalize the email (strip whitespace, downcase).
- Exclude inactive accounts.
- Write the cleaned list to a CSV.
Using filter_map we condensed the first three steps into a single, readable block:
clean_emails = users.filter_map do |u|
next nil unless u.active
email = u.email.strip.downcase
email.presence
end
Then we simply wrote:
CSV.open('contacts.csv', 'w') do |csv|
clean_emails.each { |e| csv << [e] }
end
The job ran faster, used less memory, and any future maintainer could see exactly what data made it into the export without digging through a chain of select and map.
Advanced Tips and Gotchas
While filter_map is straightforward, a few nuances can help you use it even more effectively:
- Return falsey values intentionally: If you want to drop an element based on a condition that isn’t simply
nil, you can returnnilexplicitly. For example,filter_map { |x| x if x > 0 }will keep only positive numbers becausenilis falsey. - Combine with
grepfor pattern matching:array.grep(/pattern/).filter_map { |m| m.upcase }first narrows the collection, then transforms it. - Avoid over‑nesting: If your block becomes too complex, consider extracting a helper method or using multiple enumerables. The goal is clarity, not cleverness.
Also remember that filter_map is defined in Enumerable, so it works on any collection that can be enumerated, including custom iterators.
Conclusion
Mastering filter_map gives you a powerful, idiomatic tool for cleaning and shaping collections in Ruby. It reduces boilerplate, improves performance, and makes your code read like a description of the data flow. Next time you find yourself chaining select and map, let filter_map do the heavy lifting and watch your pipelines become both faster and easier to understand.