Syntactic sugar methods
in Ruby

Welcome to a new Ruby Magic article! In this episode, we'll look at how Ruby uses syntactic sugar to make some of its syntax more expressive, or easier to read. At the end, we'll know how some of Ruby's tricks work under the hood and how to write our own methods that use a bit of this sugar.

When writing Ruby apps it's common to interact with class attributes, arrays and hashes in a way that may feel non-standard. How would we define methods to assign attributes and fetch values from an array or hash?

Ruby provides a bit of syntactic sugar to make these method work when calling them. In this post we'll explore how that works.

person1 = Person.new
person1.name = "John"

array = [:foo, :bar]
array[1]  # => :bar

hash = { :key => :foo }
hash[:key] # => :foo
hash[:key] = :value

Method names

Let's start with method names. In Ruby, we can use all kinds of characters and special symbols for method names that aren't commonly supported in other languages. If you've ever written a Rails app you've probably encountered the save! method. This isn't something specific to Rails, but it demonstrates support for the ! character in Ruby method names.

The same applies to other symbols such as =, [, ], ?, %, &, |, <, >, *, -, + and /.

Support for these characters means we can incorporate them into our method names to be more explicit about what they're for:

Defining attribute methods

When defining an attribute on a class with attr_accessor, Ruby creates a reader and a writer method for an instance variable on the class.

class Person
  attr_accessor :name
end

person = Person.new
person.name = "John"
person.name # => "John"

Under the hood, Ruby creates two methods:

Now let's say we want to customize this behavior. We won't use the attr_accessor helper and define the methods ourselves.

class AwesomePerson
  def name
    "Person name: #{@name}"
  end

  def name=(value)
    @name = "Awesome #{value}"
  end
end

person = AwesomePerson.new
person.name = "Jane"
person.name # => "Person name: Awesome Jane"

The method definition for name= is roughly the same way you would write it when calling the method person.name = "Jane". We don't define the spaces around the equals sign = and don't use parentheses when calling the method.

puts "Hello!" if (true) # With optional parentheses
puts "Hello!" if true   # Without parentheses
def greeting name # Parentheses omitted
  "Hello #{name}!"
end

greeting("Robin") # With parentheses
greeting "Robin"  # Without parentheses
greeting"Robin"   # Without parentheses and spaces

All the following ways of calling the method are supported, but we commonly omit the parentheses and add spaces to make the code a bit more readable.

# Previous method definition:
# def name=(value)
#   @name = "Awesome #{value}"
# end

person.name = "Jane"
person.name="Jane"
person.name=("Jane") # That looks a lot like the method definition!

We've now defined custom attribute reader and writer methods for the name attribute. We can customize the behavior as needed and perform transformations on the value directly when assigning the attribute rather than having to use callbacks.

Defining [ ] methods

The next thing we'll look at are the square bracket methods [ ] in Ruby. These are commonly used to fetch and assign values to Array indexes and Hash keys.

hash = { :foo => :bar, :abc => :def }
hash[:foo]        # => :bar
hash[:foo] = :baz # => :baz

array = [:foo, :bar]
array[1] # => :bar

Let's look at how these methods are defined. When calling hash[:foo] we are using some Ruby syntactic sugar to make that work. Another way of writing this is:

hash = { :foo => :bar }
hash.[](:foo)
hash.[]=(:foo, :baz)
# or even:
hash.send(:[], :foo)
hash.send(:[]=, :foo, :baz)

Compared with the way we normally write this (hash[:foo] and hash[:foo] = :baz) we can already see some differences. In the first example (hash.[](:foo)) Ruby moves the first argument between the square brackets (hash[:foo]). When calling hash.[]=(:foo, :baz) the second argument is passed to the method as the value hash[:foo] = :baz.

Knowing this, we can now define our own [ ] and [ ]= methods the way Ruby will understand it.

class MyHash
  def initialize
    @internal_hash = {}
  end

  def [](key)
    @internal_hash[key]
  end

  def []=(key, value)
    @internal_hash[key] = value
  end
end

Now that we know these methods are normal Ruby methods, we can apply the same logic to them as any other method. We can even make it do weird things like allow multiple keys in the [ ] method.

class MyHash
  def initialize
    @internal_hash = { :foo => :bar, :abc => :def }
  end

  def [](*keys)
    @internal_hash.values_at(*keys)
  end
end

hash = MyHash.new
hash[:foo, :abc] # => [:bar, :def]

Create your own

Now that we know a bit about Ruby's syntactic sugar, we can apply this knowledge to create our own methods such as custom writers, Hash-like classes and more.

You may be surprised how many gems define methods such as the square brackets methods to make something feel like an Array or Hash when it really isn't. One example is setting a flash message in a Rails application with:
flash[:alert] = "An error occurred". In the AppSignal gem we use this ourselves on the Config class as a shorthand for fetching the configuration.

This concludes our brief look at the syntactic sugar for method definition and calling in Ruby. We'd love to know how you liked this article, if you have any questions about it, and what you'd like to read about next, so be sure to let us know at @AppSignal.