Overview
Array compaction removes unwanted values from arrays through several core methods. Ruby provides Array#compact
and Array#compact!
for removing nil values, while Rails extends this functionality with compact_blank
methods that remove nil, empty strings, and other blank values.
The compact
method creates a new array with nil values removed, while compact!
modifies the original array in place. Both methods preserve the order of remaining elements and handle nested arrays by only examining top-level elements.
# Basic nil removal
[1, nil, 2, nil, 3].compact
# => [1, 2, 3]
# In-place modification
arr = [1, nil, 2, nil, 3]
arr.compact!
# => [1, 2, 3]
# arr is now [1, 2, 3]
Rails applications have access to additional compaction methods through ActiveSupport. The compact_blank
method removes nil values, empty strings, empty arrays, empty hashes, and other objects that respond to blank?
with true.
# Rails/ActiveSupport compact_blank
[1, nil, "", [], {}, " ", 2].compact_blank
# => [1, " ", 2]
Array compaction operates at the top level only - nested nil values within sub-arrays remain untouched. The methods maintain the original array's class when creating new instances, preserving subclass behavior.
Basic Usage
The compact
method handles the most common compaction scenario by removing nil values without modifying the original array. This approach works well when the original array must remain unchanged or when chaining multiple operations.
user_ids = [1, 2, nil, 4, nil, 6]
valid_ids = user_ids.compact
# Original array unchanged
puts user_ids
# => [1, 2, nil, 4, nil, 6]
puts valid_ids
# => [1, 2, 4, 6]
# Chaining with other methods
User.where(id: user_ids.compact.first(3))
The compact!
method modifies arrays in place, reducing memory usage when the original array is no longer needed. This method returns the modified array or nil if no changes occurred.
data = fetch_user_data() # Returns [1, nil, 2, nil, 3]
result = data.compact!
# data is now modified
puts data
# => [1, 2, 3]
puts result
# => [1, 2, 3]
# Returns nil when no nils present
clean_data = [1, 2, 3]
result = clean_data.compact!
# => nil (no changes made)
When working with form data or user input, compaction often combines with other array methods to clean and process data streams.
# Processing form submission with multiple optional fields
form_values = params.values_at(:field1, :field2, :field3, :field4)
# => ["John", nil, "", "Doe"]
# Remove nils but keep empty strings for validation
cleaned = form_values.compact
# => ["John", "", "Doe"]
# Further processing with reject for empty strings
final_values = cleaned.reject(&:empty?)
# => ["John", "Doe"]
Rails applications can use compact_blank
for more comprehensive cleaning when blank values should be treated equivalently to nil values.
# User profile data with various blank values
profile_data = [
user.name, # "John"
user.middle_name, # nil
user.nickname, # ""
user.bio, # " "
user.website, # "https://example.com"
user.tags # []
]
# Standard compact keeps empty strings and whitespace
profile_data.compact
# => ["John", "", " ", "https://example.com", []]
# compact_blank removes all blank values
profile_data.compact_blank
# => ["John", "https://example.com"]
Performance & Memory
Array compaction performance scales linearly with array size, but memory behavior differs between compact
and compact!
. The compact
method allocates a new array, potentially doubling memory usage temporarily, while compact!
modifies in place with minimal memory overhead.
For large arrays, compact!
provides better memory efficiency when the original array can be modified:
# Memory-efficient approach for large datasets
large_dataset = load_million_records() # Array with 1M elements, ~20% nil
# Inefficient - creates duplicate array
cleaned_copy = large_dataset.compact # Uses ~2x memory temporarily
# Efficient - modifies in place
large_dataset.compact! # Minimal additional memory usage
Benchmark comparisons show compact!
performing 15-20% faster than compact
for arrays over 10,000 elements due to reduced memory allocation and garbage collection pressure.
require 'benchmark'
# Test with 100k element array containing 20k nils
test_array = Array.new(100_000) { rand(5) == 0 ? nil : rand(1000) }
Benchmark.bmbm do |x|
x.report("compact") do
1000.times { test_array.dup.compact }
end
x.report("compact!") do
1000.times { test_array.dup.compact! }
end
end
# Typical results:
# user system total real
# compact 0.891000 0.052000 0.943000 ( 0.947621)
# compact! 0.743000 0.031000 0.774000 ( 0.778394)
When processing data in batches, compact operations can be strategically placed to minimize memory pressure:
# Processing large CSV file with missing values
def process_large_csv(filename)
results = []
CSV.foreach(filename, headers: true).each_slice(1000) do |batch|
# Process batch and compact to remove failed records
processed = batch.map { |row| process_record(row) }.compact!
# Accumulate results in smaller chunks
results.concat(processed)
# Periodic compaction of main results array
results.compact! if results.size % 10_000 == 0
end
results.compact! # Final cleanup
end
Memory profiling shows that compact_blank
carries additional overhead due to the more complex blank value detection, making it 2-3x slower than standard compact
but still linear in complexity.
Production Patterns
Production applications frequently use array compaction in data processing pipelines, API response cleaning, and batch operations. Database query results often contain nil values from LEFT JOINs or optional associations that require filtering before further processing.
class ReportGenerator
def generate_monthly_stats
# Fetch data with potential nils from outer joins
user_stats = User.joins("LEFT JOIN subscriptions ON users.id = subscriptions.user_id")
.pluck(:name, :subscription_id, :subscription_type)
# Compact subscription data while preserving user info
active_users = user_stats.map do |name, sub_id, sub_type|
{
name: name,
subscription: [sub_id, sub_type].compact.join('-')
}
end.reject { |user| user[:subscription].empty? }
generate_report(active_users)
end
end
API controllers commonly compact arrays before JSON serialization to reduce payload size and eliminate undefined values that cause client-side issues:
class Api::UsersController < ApplicationController
def index
users = User.includes(:profile, :settings).limit(50)
render json: users.map do |user|
{
id: user.id,
name: user.name,
profile_data: [
user.profile&.bio,
user.profile&.website,
user.profile&.location
].compact,
preferences: user.settings&.preferences&.compact || []
}
end
end
end
Background job processing uses compaction for error handling and result aggregation, particularly when processing arrays of items where some operations may fail:
class BatchProcessingJob < ApplicationJob
def perform(item_ids)
results = item_ids.map do |id|
begin
process_item(id)
rescue ProcessingError => e
Rails.logger.warn "Failed to process item #{id}: #{e.message}"
nil # Return nil for failed items
end
end
# Compact removes failed items, leaving only successful results
successful_results = results.compact
# Update batch status with success count
update_batch_status(
total: item_ids.size,
successful: successful_results.size,
failed: item_ids.size - successful_results.size
)
# Process successful results further
aggregate_results(successful_results) if successful_results.any?
end
end
Monitoring compaction operations in production helps identify data quality issues and processing bottlenecks:
class CompactionMetrics
def self.track_compaction(array, context)
original_size = array.size
compacted = array.compact
nil_count = original_size - compacted.size
if nil_count > 0
StatsD.increment('array.compaction.performed',
tags: ["context:#{context}"])
StatsD.gauge('array.compaction.nil_ratio',
nil_count.to_f / original_size,
tags: ["context:#{context}"])
end
compacted
end
end
# Usage in data processing
processed_records = CompactionMetrics.track_compaction(
raw_records.map(&:process),
'user_import'
)
Common Pitfalls
Array compaction only removes top-level nil values, leaving nested nils untouched. This behavior surprises developers who expect deep compaction across nested structures.
nested_array = [1, [2, nil, 3], nil, 4]
result = nested_array.compact
# => [1, [2, nil, 3], 4] # Nested nil remains
# For deep compaction, use recursive approach
def deep_compact(array)
array.map do |element|
element.is_a?(Array) ? deep_compact(element) : element
end.compact
end
deep_compact(nested_array)
# => [1, [2, 3], 4]
The compact!
method returns nil when no modifications occur, not the array itself. This trips up developers who chain operations or check return values:
clean_array = [1, 2, 3]
result = clean_array.compact!
# => nil (no nils to remove)
# Incorrect chaining - fails because result is nil
result.first(2) # NoMethodError: undefined method `first' for nil
# Correct approaches
clean_array.compact! || clean_array # Return original if no changes
# or
(clean_array.compact!; clean_array) # Always return the array
Rails compact_blank
method removes more values than expected. Empty strings, whitespace-only strings, empty arrays, and empty hashes are all considered blank:
mixed_data = ["John", "", " ", [], {}, false, 0]
# Standard compact only removes nil
mixed_data.compact
# => ["John", "", " ", [], {}, false, 0]
# compact_blank removes empty/whitespace strings and containers
mixed_data.compact_blank
# => ["John", false, 0] # Note: false and 0 remain
Performance degrades when repeatedly compacting the same array without checking if compaction is needed. This commonly occurs in iterative processing:
# Inefficient - compacts on every iteration
data = load_initial_data()
10.times do |i|
data = process_iteration(data)
data.compact! # May do nothing if no nils added
end
# Efficient - track if compaction needed
data = load_initial_data()
needs_compaction = false
10.times do |i|
old_size = data.size
data = process_iteration(data)
needs_compaction ||= data.size != old_size || data.any?(&:nil?)
end
data.compact! if needs_compaction
Type assumptions after compaction can cause errors when the array becomes empty or when non-nil values don't match expected types:
user_ids = [nil, nil, nil]
valid_ids = user_ids.compact # => []
# Dangerous assumption - may fail with empty array
first_user = User.find(valid_ids.first) # Error: can't find nil
# Safe approach with presence check
if valid_ids.any?
first_user = User.find(valid_ids.first)
end
# Or use safe navigation
first_user = User.find(valid_ids.first) if valid_ids.first
Reference
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Array#compact |
None | Array |
Returns new array with nil values removed |
Array#compact! |
None | Array or nil |
Removes nil values in place, returns modified array or nil if unchanged |
ActiveSupport Extensions
Method | Parameters | Returns | Description |
---|---|---|---|
Array#compact_blank |
None | Array |
Returns new array with blank values removed (nil, "", [], {}, etc.) |
Array#compact_blank! |
None | Array or nil |
Removes blank values in place, returns modified array or nil if unchanged |
Blank Value Detection
Values considered blank by compact_blank
:
Value Type | Example | Blank? |
---|---|---|
nil |
nil |
Yes |
Empty String | "" |
Yes |
Whitespace String | " " |
Yes |
Empty Array | [] |
Yes |
Empty Hash | {} |
Yes |
False | false |
No |
Zero | 0 |
No |
Empty Set | Set.new |
Yes |
Return Value Patterns
Method | Has Changes | No Changes | Array State |
---|---|---|---|
compact |
New filtered array | New array (same content) | Original unchanged |
compact! |
Modified array | nil |
Original modified or unchanged |
compact_blank |
New filtered array | New array (same content) | Original unchanged |
compact_blank! |
Modified array | nil |
Original modified or unchanged |
Performance Characteristics
Operation | Time Complexity | Space Complexity | Notes |
---|---|---|---|
compact |
O(n) | O(k) where k = non-nil elements | Creates new array |
compact! |
O(n) | O(1) | Modifies in place |
compact_blank |
O(n) | O(k) where k = non-blank elements | More expensive blank checking |
compact_blank! |
O(n) | O(1) | Most expensive due to in-place blank checking |
Common Usage Patterns
# Safe chaining with compact!
array.compact! || array
# Conditional compaction
array.compact! if array.any?(&:nil?)
# Deep compaction
def deep_compact(obj)
case obj
when Array
obj.map { |item| deep_compact(item) }.compact
when Hash
obj.transform_values { |value| deep_compact(value) }
else
obj
end
end
# Batch processing with compaction
items.each_slice(100) do |batch|
results = batch.map(&:process).compact!
save_results(results) if results&.any?
end