In Progress
Unit 1, Lesson 1
In Progress

Dangerous Method

One of Ruby’s syntactical peculiarities is that it allows you to end method names with a question mark (?) or an exclamation point (!), aka “bang”. The conventions surrounding the use of question-mark method names are pretty straightforward. But the conventions for “bang methods” are a bit more subtle.

If you don’t learn the rules for bang methods, you’ll be in for confusion and frustration. And you may confuse other programmers in turn. In this episode, we’ll demystify when and when not to end a method with a bang.

Video transcript & code

One of the first things many programmers notice when they come to Ruby from another language is that some methods include punctuation characters as part of their name.

For instance, some methods have a question mark at the end. This is a convention Ruby inherited from the Scheme programming language.

After a little bit of experience with Ruby we quickly learn the rules for question mark methods. We learn that this character nearly always flags a "predicate method". This means a method which returns a value that should be treated as the true or false answer to a question. And we learn that with very few exceptions, all predicate methods end with the question mark.

123.odd?                        # => true
123.nil?                        # => false

There's another bit of punctuation sometimes found in method names: the exclamation point, or "bang".

s = "abc"
s.reverse!
s                               # => "cba"

As we start to learn the language and it's libraries, we might start to get the idea that this idiom has a meaning that's just as simple and consistent as the question mark.

We notice that anywhere there is a method which will return a modified copy of a data structure…, leaving the original intact , there is also a "bang version" which modifies the object in-place.

s = "avdi"
s.capitalize                    # => "Avdi"
s                               # => "avdi"

s.capitalize!
s                               # => "Avdi"

And as we explore the language further, we see that this convention appears in various different data structures, including arrays… and hashes.

a = [4, 3, 5, 7, 2, 9, 1]
a.sort                          # => [1, 2, 3, 4, 5, 7, 9]
a                               # => [4, 3, 5, 7, 2, 9, 1]

a.sort!
a                               # => [1, 2, 3, 4, 5, 7, 9]

h = {roses: "red"}
h.merge(violets: "blue")        # => {:roses=>"red", :violets=>"blue"}
h                               # => {:roses=>"red"}

h.merge!(daffodils: "yellow")
h                               # => {:roses=>"red", :daffodils=>"yellow"}

But then, just as we think we've got a handle on this bang convention, Ruby starts throwing us curveballs.

We discover that there are some methods, such as Array#shift, which mutate an object's state but which don't end in bang.

a = [:larry, :moe, :curly]
a.shift                         # => :larry
a                               # => [:moe, :curly]

We also start to run into methods where the "bang" seems to signal something other than state mutation.

For instance, the Kernel exit method exits the program normally…

…whereas the bang version exits immediately, without cleaning up or running any at_exit handlers.

exit                            # Clean exit
exit!                           # Immediate exit

And in the tempfile library, close with no bang just closes the file, whereas close! with a bang closes it and then deletes it.

require "tempfile"

tmp = Tempfile.new("file")
# => #<Tempfile:/tmp/20160926-13143-1l66nxp>

tmp.close                       # just close
tmp.close!                      # close and delete

And then if we start using libraries like ActiveRecord or ActiveSupport, we run into cases where the "non-bang" version won't raise any exceptions, but the "bang" version can raise errors.

require "active_support"
require "active_support/core_ext"

"not a file".try(:close)
# => nil

"not a file".try!(:close)

# ~> NoMethodError
# ~> undefined method `close' for "not a file":String
# ~>
# ~> /home/avdi/.gem/ruby/2.3.0/gems/activesupport-4.2.6/lib/active_support/core_ext/object/try.rb:77:in `public_send'
# ~> /home/avdi/.gem/ruby/2.3.0/gems/activesupport-4.2.6/lib/active_support/core_ext/object/try.rb:77:in `try!'
# ~> xmptmp-in11642IUS.rb:7:in `<main>'

It's at this point that many beginning Ruby programmers start to get frustrated with the seeming lack of consistency in use of this "bang" convention. But the real problem here is one of expectations.

Because the exclamation point suffix for Ruby methods was never meant to indicate that a method was specifically a mutating command. Rather, the bang is more of a generic warning flag.

Wherever there are two different variants of a single method, the bang version indicates that this version does something surprising and potentially dangerous. As Matz, the creator of Ruby, wrote in a forum post years ago:

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".

Another way of looking at it is that when you run across an unfamiliar bang version of a method while reading some code, that's a sign that you should probably check the documentation to find out what it means.

It also follows from this explanation of bang methods that when you're designing your own object interfaces, it is only idiomatic to give them bang methods if there is also a non-bang version. Since the bang, by definition, means a "more dangerous variant", there should always be a non-bang version for it to be a variant of.

class Beer
  attr_reader :empty

  def initialize
    @empty = false
  end

  def drink
    dup.tap{|b| b.drink!}
  end

  def drink!
    @empty = true
  end

  def inspect
    empty ? "#<Empty Beer>" : "#<Full Beer>"
  end
end
b = Beer.new
b.drink
# => #<Empty Beer>
b
# => #<Full Beer>

b.drink!
b
# => #<Empty Beer>

If it only makes sense for the object to have a single version of the method, even if it's a mutating method, leave the bang off of the end.

class Beer
  attr_reader :empty

  def initialize
    @empty = false
  end

  def drink
    @empty = true
  end

  def inspect
    empty ? "#<Empty Beer>" : "#<Full Beer>"
  end
end
b = Beer.new
b.drink
b
# => #<Empty Beer>

Keep these guidelines in mind, and you'll have an easier time understanding the meaning of bang methods in idiomatic Ruby code. And you'll write code that conveys your intent accurately to other Ruby programmers.

Happy hacking!

Responses