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.

ruby
person1 = Person.new person1.name = "John" array = [:foo, :bar] array[1] # => :bar hash = { :key => :foo } hash[:key] # => :foo hash[: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.

ruby
class Person attr_accessor :name end person = Person.new person.name = "John" person.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.

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

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.

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

The same applies to method definitions and other expressions.

ruby
def greeting name # Parentheses omitted "Hello #{name}!" end greeting("Robin") # With parentheses greeting "Robin" # Without parentheses greeting"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.

ruby
# 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.

ruby
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:

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

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

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

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

Become our next author!

Find out more

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