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.
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.
Under the hood, Ruby creates two methods:
Person#name
for reading the attribute/instance variable on the class usingattr_reader
, and;Person#name=
for writing the attribute/instance variable on the class usingattr_writer
.
Now let's say we want to customize this behavior. We won't use the attr_accessor
helper and define the methods ourselves.
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.
The same applies to method definitions and other expressions.
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.
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.
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:
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.
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.
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.