Array#flatten!
array.flatten!(level=nil) -> array or nil The flatten! method transforms a nested structure into a single flat sequence, modifying the original object in place. It recursively flattens all sub-collections until the result is one-dimensional.
How it works
flatten! takes an optional depth parameter. Without arguments, it flattens all levels of nesting. With a depth, it only flattens that many levels.
Practical Examples
Basic Flattening
array = [1, [2, 3], [4, [5, 6]]]
array.flatten!
puts array.inspect # => [1, 2, 3, 4, 5, 6]
When you pass no argument, flatten! recurses through every level of nesting until the receiver is completely one-dimensional. It modifies the original object in place, so any other variable holding a reference to the same collection will see the flattened result.
nested = [[1, 2], [[3, 4]], [[[5]]]]
nested.flatten! # => [1, 2, 3, 4, 5]
The default call flattens all the way down, which covers the common case of collapsing a deeply nested structure into a simple list. When you only want to remove one or two levels of wrapping, the depth argument gives you finer control.
With depth parameter
data = [1, [2, 3], [[4, 5]]]
data.flatten!(1)
# => [1, 2, 3, [4, 5]] - inner nesting preserved
Passing a positive integer limits how far flattening recurses. Only nesting within the specified depth is collapsed, while deeper structures stay intact. This is handy when you need to unwind one layer of a tree without disturbing the deeper branches.
original = [1, [2, 3]]
copy = original.flatten!(0)
# => [1, [2, 3]]
Setting a depth works well when the nesting follows a predictable pattern, like rows inside groups. In practice, many scripts skip the argument entirely because they need to flatten everything for iteration, and the extra precision just adds visual noise without changing the result.
Practical data processing
groups = [[1, 2], [3, 4], [5, 6]]
groups.flatten! # => [1, 2, 3, 4, 5, 6]
Flattening is common when combining results from multiple sources. After gathering sub-lists from separate database queries or API calls, flatten! collapses them into a single sequence for downstream processing. This same technique handles tree-like structures, where you might build nested branches during an initial pass and then flatten everything before iteration.
tree = [:root, [:child1, :child2], [:child3, [:grandchild]]]
tree.flatten! # => [:root, :child1, :child2, :child3, :grandchild]
These grouping examples assume a nested input, but the method also has to handle the case where nothing needs doing. Understanding what flatten! returns in that situation matters because code that unwraps a multi-source collection must be prepared for data that arrives already flat.
Returns nil when no change needed
flat = [1, 2, 3]
result = flat.flatten!
puts result # => nil
The nil return follows Ruby’s convention for bang methods: when no modification is necessary, the method signals this by returning nil instead of the object. This lets you write conditional logic around whether flattening actually took place, which is valuable when the shape of incoming data is unpredictable.
list = [1, 2, 3]
if list.flatten!
puts "List was flattened"
else
puts "Already flat"
end
The conditional check in the second example relies on nil being falsy in Ruby, a pattern that appears throughout the standard library. This is one reason the non-bang flatten exists alongside the bang version: you pick the form that matches whether you want a new object or an in-place update.
Comparison with flatten (non-destructive)
original = [1, [2, 3]]
new_array = original.flatten
puts original # => [1, [2, 3]] — unchanged
puts new_array # => [1, 2, 3]
The non-bang flatten method leaves the original untouched and allocates a fresh collection. This is the safer default when other code depends on the original structure remaining intact after the call. Choosing between the two versions comes down to whether mutation is the point or a side effect.
original.flatten!
puts original # => [1, 2, 3] — modified
The in-place mutation handles elements that are not themselves collections without raising an error. When the input mixes scalars with nested sub-lists, only the sub-lists get unwound, and the rest pass through unchanged. This makes the method safe to call on heterogeneous input where the nesting depth varies across positions.
Working with mixed data
mixed = [1, "hello", [2, 3], {key: "value"}]
mixed.flatten! # => [1, "hello", 2, 3, {:key=>"value"}]
Performance Considerations
For deeply nested structures, flatten! can be memory-intensive. If you need lazy flattening, consider using a loop or recursion that processes elements one at a time.
Flattening in place when the original array should change
flatten! is the right choice when the nested structure is already the working collection and the caller wants that same object updated. That can be useful after collecting data from several sources or after building a tree-like structure that now needs to become a simple list. The method makes the mutation explicit, which helps the reader understand that later code will see the flattened result in the original variable.
The return value is also worth watching because flatten! can return nil if no change was needed. That makes it useful for checking whether any flattening actually happened, but it also means the surrounding code should not assume a new collection was always produced. The in-place behavior is the main point: change the structure you already have, then continue with the simpler shape.
# Building a result list from several passes, then collapsing in place
results = []
[1, 2, 3].each { |n| results << [n, n * 10] }
results.flatten! # => [1, 10, 2, 20, 3, 30]
That in-place behavior also means the method is best used when the caller really wants the original object to change for everyone who still holds a reference to it. If that shared mutation would be confusing, the non-destructive version is the safer choice. The bang form is strongest when the code wants a clear, deliberate update rather than a fresh copy.
That distinction is what keeps the method useful in real code. The destructive form is not just a faster version of the non-destructive one; it is a choice about ownership and visibility. When the surrounding code already treats the collection as the working value, flatten! can keep the flow short and direct while still keeping the change obvious.
The method is especially clear in code that prepares a result for later steps, because the flattening rule is visible right where the shape changes. That keeps the transformation honest and prevents a hidden copy from appearing where the caller expected mutation. In small workflows, that directness is often exactly what makes the in-place version worth using.
It is also a useful reminder that mutation can be a feature when the collection is already the shared working value. In that case, changing the original object is not a surprise; it is the point of the operation. That is why the bang form makes the intent easy to read in code that owns the structure throughout.