On the importance of syntax and semantics; How Ruby's power lies in its beauty
I was chatting to a colleague recently who expressed they didn’t really like Ruby’s excessive list of keywords.
I was surprised that this was their opinion (especially given their background in JavaScript). Ruby has, compared to many other popular languages, a relatively terse syntax, and many of its keywords (like undef
, redo
, or retry
) are rarely used in typical workflows.
This chart below shows Ruby’s keywords (there are 41), compared to other common languages:
Image source: https://github.com/e3b0c442/keywords
Ruby’s syntax and grammar is often the reason that so many programmers fall in love with Ruby.
The Ruby language was designed to optimise for programmer happiness rather than machine efficiency, and it achieves this with an elegant, poetic syntax that is very easy to read and write.
Compare these two snippets of code, both of which define a simple toy robot that takes instructions and moves on a table.
class TabletopRobot
DIRECTIONS = %w[NORTH EAST SOUTH WEST].freeze
def initialize
@x = nil
@y = nil
@facing = nil
end
def place(x, y, facing)
return unless valid_position?(x, y) && DIRECTIONS.include?(facing)
@x = x
@y = y
@facing = facing
end
def move
return unless placed?
case @facing
when 'NORTH' then @y += 1 if valid_position?(@x, @y + 1)
when 'EAST' then @x += 1 if valid_position?(@x + 1, @y)
when 'SOUTH' then @y -= 1 if valid_position?(@x, @y - 1)
when 'WEST' then @x -= 1 if valid_position?(@x - 1, @y)
end
end
def left
return unless placed?
idx = (DIRECTIONS.index(@facing) - 1) % DIRECTIONS.size
@facing = DIRECTIONS[idx]
end
def right
return unless placed?
idx = (DIRECTIONS.index(@facing) + 1) % DIRECTIONS.size
@facing = DIRECTIONS[idx]
end
def report
return unless placed?
"#{@x},#{@y},#{@facing}"
end
private
def placed?
!@x.nil? && !@y.nil? && !@facing.nil?
end
def valid_position?(x, y)
x.between?(0, 4) && y.between?(0, 4)
end
end
Now, here’s the same logic implemented in C++:
#include <iostream>
#include <string>
#include <array>
#include <optional>
class TabletopRobot {
public:
void place(int x, int y, const std::string& facing) {
if (validPosition(x, y) && isValidDirection(facing)) {
this->x = x;
this->y = y;
this->facing = facing;
}
}
void move() {
if (!isPlaced()) return;
if (facing == "NORTH" && validPosition(x, y + 1)) {
y += 1;
} else if (facing == "EAST" && validPosition(x + 1, y)) {
x += 1;
} else if (facing == "SOUTH" && validPosition(x, y - 1)) {
y -= 1;
} else if (facing == "WEST" && validPosition(x - 1, y)) {
x -= 1;
}
}
void left() {
if (!isPlaced()) return;
int idx = (directionIndex(facing) + 3) % 4;
facing = DIRECTIONS[idx];
}
void right() {
if (!isPlaced()) return;
int idx = (directionIndex(facing) + 1) % 4;
facing = DIRECTIONS[idx];
}
std::optional<std::string> report() const {
if (!isPlaced()) return std::nullopt;
return std::to_string(x) + "," + std::to_string(y) + "," + facing;
}
private:
int x = -1;
int y = -1;
std::string facing = "";
const std::array<std::string, 4> DIRECTIONS = {"NORTH", "EAST", "SOUTH", "WEST"};
bool isPlaced() const {
return facing != "";
}
bool validPosition(int x, int y) const {
return x >= 0 && x <= 4 && y >= 0 && y <= 4;
}
bool isValidDirection(const std::string& dir) const {
for (const auto& d : DIRECTIONS) {
if (d == dir) return true;
}
return false;
}
int directionIndex(const std::string& dir) const {
for (int i = 0; i < 4; ++i) {
if (DIRECTIONS[i] == dir) return i;
}
return -1; // Should not happen if dir is validated
}
};
I may be biased, but I suspect most programmers would find the Ruby example easier to read and understand than the C++ version. Even non-programmers might be able to make a reasonable guess at what the Ruby code does, thanks to its natural, English-like syntax (e.g., valid_position?, placed?, move). The C++ version, with its angle brackets, type declarations, and explicit memory management constructs, is far less approachable.
Syntax vs. Semantics: The Key Difference
But these two code classes are more or less the same, in terms of their naming and the behaviour they define—in other words, they are semantically similar. The difference, then, is the syntax (grammar and meta-instructions) defined by the language that make Ruby so much easier to read and understand than C++.
Let’s compare the two report()
methods to highlight this contrast:
std::optional<std::string> report() const {
if (!isPlaced()) return std::nullopt;
return std::to_string(x) + "," + std::to_string(y) + "," + facing;
}
def report
return unless placed?
"#{@x},#{@y},#{@facing}"
end
In C++, the method declaration includes a return type (std::optionalstd::string
), a const modifier, and explicit string conversions (std::to_string
). The use of std::nullopt
to handle the “not placed” case adds another layer of complexity. In Ruby, the same logic is expressed in three lines with minimal syntax: no return type declaration, no semicolons, and a concise string interpolation syntax (#{...}
). Ruby’s unless keyword reads like natural language, making the intent immediately clear.
This difference is not accidental. Ruby was designed specifically with programmer happiness in mind, and was therefore designed to be as easy to read and reason about as possible.
“I hope to see Ruby help every programmer in the world to be productive, and to enjoy programming, and to be happy.” —Matz (Yukihiro Matsumoto)
This aspect of Ruby code is not simply aesthetic. The argument being made here is not simply “Ruby is good because it looks good”. A simple language that is easy to read and reason about means that you, the programmer, have more mental capacity to focus on solving your domain problem, instead of hunting for a missing semicolon.
Comparing Ruby to Other Lightweight Languages
Of course, Ruby is not the simplest language, in terms of the lack of syntax. Notably, Elixir and Go are very slim languages (and very popular too). The SmallTalk language was famously small enough to fit on a postcard, and you can sit down to learn the entire Go language before your cup of tea goes cold.
But Go and Elixir lack some of the expressive power that Ruby has, in terms of it’s dynamic metaprogramming and introspection capabilities—things that make Ruby incredibly powerful and flexible.
Syntax matters.
Ruby strikes a remarkable balance between a lightweight, readable syntax and the dynamic power of meta-programming and introspection. It allows developers to write expressive, poetic code while maintaining the flexibility to tackle complex problems. Compared to languages like Elixir and Go, Ruby offers more dynamic features; compared to C++, it offers a far more approachable syntax—which translates to programmer happiness and productivity.
By keeping its syntax minimal yet expressive, Ruby maximizes productivity and lets programmers focus on solving problems rather than fighting the language. As Matz intended, Ruby helps programmers not just to be productive, but to enjoy the process—and that’s what makes it such a special language.
Further reading
Bob Martin (who has written code for over 50 years, and is a signatory of the Agile Manifesto) has a great post about why he loves Clojure so much.
https://blog.cleancoder.com/uncle-bob/2019/08/22/WhyClojure.html
The main argument is the “economy of expression”. There’s very little syntax or grammar in Clojure. This means the code that you write is mostly the code you need to solve your problem, without getting bogged down in instructions for the compiler or interpreter.
This frees up mental space and energy to focus on your problem, because you’re not having to waste time worrying about the metalanguage around your code.