Ruby's bang methods - Handle with care!
In Ruby,
method names can be suffixed with !
.
These are often called “bang” methods.
This is used to indicate
that it is a dangerous counterpart
of the non-bang version of the method.
I had always thought of “dangerous” to mean that the method mutates the underlying object and returns that object. However, these methods can differ from their non-bang versions in other subtle ways. This tripped me up recently.
The case of Array#reject!
I was modifying some code recently,
and was surprised by how Array#reject!
works.
Array#reject
- the non-bang version -
returns a new array
after removing elements for which
the block evaluates to a truthy value.
I had expected reject!
to work similarly,
with the exception that
it returns the mutated receiver.
Let’s look at an example comparing the two methods, when there is something to reject:
[1, 2, 3].reject(&:even?) #=> [1, 3]
[1, 2, 3].reject!(&:even?) #=> [1, 3]
Both of them returned an array. Now, let’s try changing the input to only contain odd numbers.
[1, 3, 5].reject(&:even?) #=> [1, 3, 5]
[1, 3, 5].reject!(&:even?) #=> nil 💥
Turns out reject!
returns nil
if there’s nothing to reject.
This is how a lot of methods
in the standard library behave.
Using bang methods safely
In the code that I was changing,
we had an intermediate array
that wasn’t being used.
I thought it was safe to mutate it
to avoid allocating more objects.
If you need to do something like this,
you can use reject!
inside tap
like this:
[1,3,5].tap { |n| n.reject!(&:even?) } #=> [1, 3, 5]
There’s also another alternative
in case of reject!
-
Array#delete_if
works like
how I expected reject!
to work
and returns the array,
irrespective of whether it was mutated or not.
[1, 2, 3].delete_if(&:even?) #=> [1, 3]
[1, 3, 5].delete_if(&:even?) #=> [1, 3, 5]
In the end, I decided not to change the code.
I find reject
to be the most readable option,
and profiling the code
didn’t show any significant gains
from avoiding the allocations.
So what does !
indicate?
Matz, the creator of Ruby, has this to say about bang methods:
The bang (!) does not mean “destructive” nor lack of it mean non destructive either. The bang sign means “the bang version is more dangerous than its non bang counterpart; handle with care”.
This is what Ruby docs say:
…by convention, a method with an exclamation point or bang is considered dangerous. In Ruby’s core library the dangerous method implies that when a method ends with a bang (!), it indicates that unlike its non-bang equivalent, permanently modifies its receiver.
Typically,
in the Ruby standard library,
this means that a bang method
returns either the mutated object,
or nil
if there’s nothing to mutate.
However,
not all dangerous methods
are bang methods.
There are Array
methods like
push
, pop
, prepend
, etc
which will mutate the object,
but they don’t have the !
suffix
because they don’t have
a safe counterpart.