Ruby Bang (!) Methods

Explaining the naming convention of ruby bang(!) methods

ยท

5 min read

Ruby is a powerful and elegant programming language, known for its readability and expressiveness. Optimised for developer happiness. One of the more unique aspects of Ruby is its "bang"(!) methods. As a newcomer to Ruby, you might find yourself wondering about the purpose of these methods and how to use them. In this article, we will explore Ruby's bang methods, the naming convention, and their use in a Ruby on Rails project.

What are Bang Methods?

In Ruby, bang methods are simply methods that have an exclamation mark (!) at the end of their names. The bang is a naming convention to signify that the method has some potentially surprising or dangerous behaviour compared to its non-bang counterpart.

Mutating bang methods

Generally, bang methods mutate/modify the object they are called upon, while their non-bang counterparts return a new object with the desired changes.

Here are a few examples of bang and non-bang method pairs in Ruby:

  1. String#upcase and String#upcase!

  2. Array#sort and Array#sort!

  3. Array#uniq and Array#uniq!

Let's take a closer look at these examples to understand the differences between bang and non-bang methods.

Example 1: String#upcase and String#upcase!

String#upcase returns a new string with all characters in uppercase, leaving the original string unchanged. In contrast, String#upcase! modifies the original string in-place, converting all characters to uppercase.

text = "hello, world!"

uppercase_text = text.upcase
puts uppercase_text # => "HELLO, WORLD!"
puts text           # => "hello, world!"

text.upcase!
puts text           # => "HELLO, WORLD!"

Example 2: Array#sort and Array#sort!

Array#sort returns a new array with its elements sorted, while Array#sort! sorts the original array in-place.

numbers = [5, 3, 1, 4, 2]

sorted_numbers = numbers.sort
puts sorted_numbers.inspect # => [1, 2, 3, 4, 5]
puts numbers.inspect        # => [5, 3, 1, 4, 2]

numbers.sort!
puts numbers.inspect        # => [1, 2, 3, 4, 5]

Example 3: Array#uniq and Array#uniq!

Array#uniq returns a new array with duplicate elements removed, while Array#uniq! removes duplicate elements from the original array in-place.

repeated_numbers = [1, 2, 2, 3, 3, 3]

unique_numbers = repeated_numbers.uniq
puts unique_numbers.inspect # => [1, 2, 3]
puts repeated_numbers.inspect        # => [1, 2, 2, 3, 3, 3]

repeated_numbers.uniq!
puts repeated_numbers.inspect        # => [1, 2, 3]

Error-raising bang methods

In a Ruby application, bang methods are also prevalent for denoting error-raising methods. You can find common use cases in a library such as ActiveRecord, the built-in Object-Relational Mapping (ORM) framework for Ruby on Rails that handles database operations. Some of the most common ActiveRecord bang methods include:

  1. ActiveRecord::Base#save and ActiveRecord::Base#save!

  2. ActiveRecord::Base#create and ActiveRecord::Base#create!

  3. ActiveRecord::Base#update and ActiveRecord::Base#update!

  4. ActiveRecord::Base#destroy and ActiveRecord::Base#destroy!

Let's examine these ActiveRecord examples to understand their usage and differences in a Ruby on Rails application.

Example 1: ActiveRecord::Base#save and ActiveRecord::Base#save!

save and save! are instance methods used to persist an ActiveRecord object to the database. The non-bang method, save, returns a boolean value indicating whether the record was saved successfully or not. If there are any validation errors, it returns false.

On the other hand, save! raises an ActiveRecord::RecordInvalid exception if the record is invalid, halting the execution flow. This method is useful when you want to ensure the record is valid and saved; otherwise, an exception should be raised.

class User < ActiveRecord::Base
  validates :email, presence: true
end

user = User.new(email: "")

# Using save
if user.save
  puts "User saved!"
else
  puts "User not saved. Errors: #{user.errors.full_messages.join(', ')}"
end

# Using save!
begin
  user.save!
  puts "User saved!"
rescue ActiveRecord::RecordInvalid => e
  puts "User not saved. Errors: #{e.record.errors.full_messages.join(', ')}"
end

Example 2: ActiveRecord::Base#create and ActiveRecord::Base#create!

create and create! are class methods used to create and save a new ActiveRecord object to the database in a single step. Similar to save, create returns the newly created object, whether it's valid or not. If the object is invalid, it won't be persisted to the database.

In contrast, create! raises an ActiveRecord::RecordInvalid exception if the object is invalid, making it suitable for situations where you want to ensure the record is valid and created; otherwise, an exception should be raised.

# Using create
user = User.create(email: "")
if user.persisted?
  puts "User created!"
else
  puts "User not created. Errors: #{user.errors.full_messages.join(', ')}"
end

# Using create!
begin
  user = User.create!(email: "")
  puts "User created!"
rescue ActiveRecord::RecordInvalid => e
  puts "User not created. Errors: #{e.record.errors.full_messages.join(', ')}"
end

Example 3: ActiveRecord::Base#update and ActiveRecord::Base#update!

update and update! are instance methods used to update the attributes of an ActiveRecord object and save it to the database. update returns a boolean value, indicating whether the record was updated successfully or not. If there are any validation errors, it returns false.

update!, like save! and create!, raises an ActiveRecord::RecordInvalid exception if the record is invalid, making it suitable for situations where you want to ensure the record is valid and updated; otherwise, an exception should be raised.

Example 4: ActiveRecord::Base#destroy and ActiveRecord::Base#destroy!

destroy and destroy! are instance methods used to delete an ActiveRecord object from the database. The non-bang method, destroy, returns the destroyed object if the deletion is successful. If the object has any before_destroy callbacks that halt the destruction process, it returns false.

In contrast, destroy! raises an ActiveRecord::RecordNotDestroyed exception if the object cannot be destroyed due to any before_destroy callbacks. This method is useful when you want to ensure the record is deleted; otherwise, an exception should be raised.

class User < ApplicationRecord
  before_destroy :check_admin

  private

  def check_admin
    throw :abort if email == "admin@example.com"
  end
end

user = User.create(email: "admin@example.com")

# Using destroy
if user.destroy
  puts "User destroyed!"
else
  puts "User not destroyed."
end

# Using destroy!
begin
  user.destroy!
  puts "User destroyed!"
rescue ActiveRecord::RecordNotDestroyed => e
  puts "User not destroyed."
end

In this example, the User model has a before_destroy callback that prevents the deletion of a user with the email "admin@example.com". When using destroy, the method returns false if the user cannot be deleted. However, when using destroy!, an ActiveRecord::RecordNotDestroyed exception is raised if the user cannot be deleted.

Conclusion

Understanding the differences between bang and non-bang methods in Ruby can empower you to write more efficient and maintainable code. Bang methods are more aggressive, often modifying objects in place or raising exceptions when operations fail. Non-bang methods are more lenient, returning modified copies of objects or allowing for flexible error handling. Knowing when to use each type of method can be useful in creating robust and reliable applications.

So, the next time you come across a bang method in Ruby, don't be afraid to use it but also be aware of its potential side effects. Happy coding!

๐Ÿง‘๐Ÿพโ€๐Ÿš€๐Ÿš€

Did you find this article valuable?

Support Unathi Chonco by becoming a sponsor. Any amount is appreciated!

ย