ruby

Syntactic sugar methods in Ruby

Tom de Bruijn

Tom de Bruijn on

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.

1person1 = Person.new
2person1.name = "John"
3
4array = [:foo, :bar]
5array[1]  # => :bar
6
7hash = { :key => :foo }
8hash[:key] # => :foo
9hash[:key] = :value

Syntactic sugar?

Syntactic sugar refers to the little bit of ✨ magic ✨ Ruby provides you in writing easier to read and more concise code. In Ruby this means leaving out certain symbols, spaces or writing some expression with a helper of some kind.

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:

  • Assigning attributes: person.name = "foo"
  • Ask questions: person.alive?
  • Call dangerous methods: car.destroy!
  • Making objects act like something they're not: car[:wheels]

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.

1class Person
2  attr_accessor :name
3end
4
5person = Person.new
6person.name = "John"
7person.name # => "John"

Under the hood, Ruby creates two methods:

  • Person#name for reading the attribute/instance variable on the class using attr_reader, and;
  • Person#name= for writing the attribute/instance variable on the class using attr_writer.

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

1class AwesomePerson
2  def name
3    "Person name: #{@name}"
4  end
5
6  def name=(value)
7    @name = "Awesome #{value}"
8  end
9end
10
11person = AwesomePerson.new
12person.name = "Jane"
13person.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.

Optional parentheses and spaces

You may have seen that in Ruby parentheses are optional a lot of the time. When passing an argument to a method, we don't have to wrap the argument in parentheses (), but we can if it's easier to read.

The if-statement is a good example. In many languages you wrap the expression the if-statement evaluates with parentheses. In Ruby, they can be omitted.

1puts "Hello!" if (true) # With optional parentheses
2puts "Hello!" if true   # Without parentheses

The same applies to method definitions and other expressions.

1def greeting name # Parentheses omitted
2  "Hello #{name}!"
3end
4
5greeting("Robin") # With parentheses
6greeting "Robin"  # Without parentheses
7greeting"Robin"   # Without parentheses and spaces

The last line is difficult to read, but it works. The parentheses and spaces are optional even when calling methods.

Just be careful not to omit every parentheses and space, some of these help Ruby understand what you mean! When in doubt, wrap your arguments in parentheses so you and Ruby know what arguments belongs to what method call.

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.

1# Previous method definition:
2# def name=(value)
3#   @name = "Awesome #{value}"
4# end
5
6person.name = "Jane"
7person.name="Jane"
8person.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.

1hash = { :foo => :bar, :abc => :def }
2hash[:foo]        # => :bar
3hash[:foo] = :baz # => :baz
4
5array = [:foo, :bar]
6array[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:

1hash = { :foo => :bar }
2hash.[](:foo)
3hash.[]=(:foo, :baz)
4# or even:
5hash.send(:[], :foo)
6hash.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.

1class MyHash
2  def initialize
3    @internal_hash = {}
4  end
5
6  def [](key)
7    @internal_hash[key]
8  end
9
10  def []=(key, value)
11    @internal_hash[key] = value
12  end
13end

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.

1class MyHash
2  def initialize
3    @internal_hash = { :foo => :bar, :abc => :def }
4  end
5
6  def [](*keys)
7    @internal_hash.values_at(*keys)
8  end
9end
10
11hash = MyHash.new
12hash[: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.

Share this article

RSS
Tom de Bruijn

Tom de Bruijn

Tom is a developer at AppSignal, organizer, and writer from Amsterdam, The Netherlands.

All articles by Tom de Bruijn

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps