Pour Some (Syntactic) Sugar On Me
March 22, 2018
Apologies to Def Leppard for, shall we say, “borrowing”, the name of one of their greatest hits to introduce this article. However, if any programming language has had a lot of sugar poured into it, surely it’s Ruby!
It might seem odd to refer to a programming language as “sugary”, but if you’re a Rubyist then you’re all about the sugar. Ruby is filled with more syntactic sugar than most languages, due to its emphasis on human comprehension over computer comprehension. The creator of Ruby, Yukihiro Matsumoto, wanted to make a language that was not only effective, but fun. And who thinks it’s fun to spend all day combing through curly braces, semicolons, brackets, parentheses, and all other manner of obscure syntax? Compilers and interpreters might love that kind of highly structured, unambiguous syntax, but it can be darn hard for humans to understand. That’s where syntactic sugar comes in—it makes a language “sweeter” in both writing and reading.
Let’s start by defining our terms. Keep in mind that “syntactic sugar” is not a technical term, but a construct meant to help us describe the way in which a language can be expressed. Put simply, syntactic sugar is syntax optimized for humans. The goal is to simplify syntax in order to make it easy to read, even if doing so diminishes some of the technical clarity. Of course, writing sugary code doesn’t mean we can skip the all-important step of understanding what is happening under the hood. So, let’s dive in!
Our discussion today will focus on peeling back some of the sugar in Ruby so we can internalize how Ruby operates. Here is a simple example.
a = 4
b = 2
c = a + b
puts c #=> 6
Seems straightforward, right? We assign integer values to variables a
and b
, and then assign the result of adding them together to variable c
. Finally, we call puts c
to print the result to the screen. But what is really happening here? Remember that virtually everything in Ruby is an object, including integers. Let’s take a closer look at variable a
:
a.class # => Integer
a.public_methods.include?(:+) # => true
If we look at a
’s class, we see that it is an instance of Integer
. We can then look at a
’s public methods and in a long list of behaviors we find one represented by the symbol :+
. The symbols here are Ruby’s way of keeping track of messages, which when sent to a given receiver, trigger some behavior. In this case, we trigger a behavior in a to take whichever accompanying argument is provided and return the result of adding that argument’s value to its own value. In other words, the +
is nothing more than an ordinary method (well, an ordinary method with lots of sugar!) If we wanted, we could achieve the same result by doing this:
c = a.+(b)
Looking at our expression this way, it is much more obvious that we’re actually using a method. Using dot notation we send a message to the object contained in a
and ask it to return to us the result of adding the value of argument b
to its own value. Phew, that was complicated. If we wanted to really be clear, we could even write this as:
c = a.send(:+, b)
But who wants to read (or write!) code like that? It’s so much simpler to just write this expression the sugary way, c = a + b
, and be done with it. That’s what syntactic sugar is all about—making the programming experience more expressive for human comprehension.
Our first example even has some more hidden sugar for us in the final line: puts c
. Even here Ruby is making things easier for us. puts
is a method too, defined on the Kernel
module, which is in turn mixed into the IO
class, which is Ruby’s input/output stream. If we were, for some reason, opposed to fun sugary code, we might have written: $stdout.puts(c)
and gotten the same result.
By now you’re starting to get the idea: syntactic sugar is a simpler way of writing otherwise complex (and often inscrutable) code. We looked at the :+
method already, and you won’t be surprised to learn that other arithmetic operators work the same way. There are :-
, :*
, :/
methods and so on. Knowing that these operations are just sugary methods can be quite handy.
Take for example the #reduce
method in the Enumerable
module. A classic use case for this method is it to iterate over an array and apply some operation to each element in a cumulative fashion. #reduce
can take either a block or a symbol to do its work. In the latter case, the symbol represents some method that we want to use. Hmm, didn’t we just discuss some interesting methods? Indeed, we can pass the :+
symbol to #reduce
, which is like saying “hey reduce, go ahead and sum up all of the elements in this array and give me the result.” Which is why an expression such as [1, 2, 3].reduce(:+)
returns the expected result of 6
. Neat! We just used our deeper understanding of what lies behind Ruby’s sugar to do something more complex!
Another classic place for sugar is in comparison operators, like ==
, <
, >
, !=
, and so on. We use these every day in even the most simple of programs. But how does Ruby know how to compare two integers as opposed to two strings? Or two arrays? Or two objects of any kind? As always, the answer is methods.
If we take a look again at the behaviors on some of our favorite classes, we can see that they have various comparison methods defined on them. Here is an example that shows that arrays and integers both have a :==
method:
[1, 2, 3].public_methods.include?(:==) # => true
42.public_methods.include?(:==) # => true
So, when you compare two arrays, or two integers, or two hashes, etc., Ruby has methods that define how to carry out the comparisons. Each method differs in implementation (since you can hardly expect arrays to compare themselves in the same way integers do), but each implementation shares the same sugary topping, which is why [1, 2, 3] == [4, 5, 6]
is a valid expression, rather than having to write [1, 2, 3].==([4, 5, 6])
.
Don’t worry, we won’t look at every bit of sugar in Ruby (we’d be here all day—Ruby is a veritable candy factory among programming languages!), but let’s look at one more key piece of sweet, sweet Ruby—getters and setters. Consider this short snippet:
arr = [“a”, “b”, “c”]
arr[1] # => “b”
arr[1] = “z”
arr[1] # => “z”
We’re all used to array element reference in this form, but have you thought about just how sugary this code really is? As with comparison and arithmetic operators, an array’s index access mechanisms are really just methods. To be precise, the :[]
and :[]=
methods. What we have above is a sugared up way of writing expressions like arr.[](1)
and arr.[]=(1, "abc")
. Looks a lot nicer the sugary way, doesn’t it?
You might be thinking to yourself, “this all seems awfully esoteric, why do I care how these operations are implemented? I just want my code to work, sugary or not.” Well, the reason we care is because understanding Ruby in depth, or any language for that matter, lets us do some really interesting things. Before we finish up, let’s see how we can make some sugar of our own. Imagine the following code:
class Lion
attr_reader :name, :age, :weight, :height, :bravery
def initialize(name, age, weight, height, bravery)
@name = name
@age = age
@weight = weight
@height = height
@bravery = bravery
end
end
simba = Lion.new(“Simba”, 10, 37, 60, 99)
scar = Lion.new(“Scar”, 42, 190, 128, 72)
puts simba > scar # => NoMethodError
Here, we define a Lion
class and assign it some basic attributes. But ack! When we attempt to compare our two lions we get a NoMethodError
! That’s because unlike integers, or arrays, or strings, Ruby has no idea how to compare lions. Thankfully, we can implement this functionality ourselves.
class Lion
include Comparable
attr_reader :name, :age, :weight, :height, :bravery
def initialize(name, age, weight, height, bravery)
@name = name
@age = age
@weight = weight
@height = height
@bravery = bravery
end
def <=>(other_lion)
self.bravery <=> other_lion.bravery
end
end
simba = Lion.new(“Simba”, 10, 37, 60, 99)
scar = Lion.new(“Scar”, 42, 190, 128, 72)
puts simba > scar # => true
Look at that beautiful sugar! Now, Ruby knows to compare lions by checking which of the two has the greater bravery (thanks to the Comparable module and the “spaceship” operator, which are topics for another post.) If we wanted, we could have defined our lion comparison by age, weight, height, or some other attribute. It’s up to you! And we can use Ruby’s handy comparison sugar rather than having some method like lion_1.greater_than?(lion_2)
. Further, by using the Comparable module we can also call sugary methods like lion_1 == lion_2
, lion_1 < lion_2
, etc.
As in real life, knowing how much sugar you’re using is important for your overall health (or rather, your code’s overall health.) Sugar keeps our code simple and expressive, but it also raises ambiguities. In the above example, we see that lions are compared based on their bravery score, but would another developer know that? What if someone expected lions to be compared by weight? They would be expecting to get a totally different result. In this case, perhaps it would have been better to write a #braver_than?
method for our lions. The decision is up to you and it’s important that you consider the clarity ramifications of too much sugar.
If you’ve gotten this far, congratulations! You’re now a sugar expert. OK, well perhaps not an expert, but at least you know a bit about spotting sugar when you see it (and sprinkling a bit of your own.) Syntactic sugar is a big part of what makes Ruby so expressive and easy to use—and we have only scratched the surface. There is a lot more sugar out there for you to consume, so get to it. Just don’t develop too much of a sweet tooth!
Note: This article was originally published on Medium.