Coming from JavaScript to Ruby, I was excited to learn the language that promised to be "friendly to developers" and "designed for developer happiness". I discovered a language which, like JS, is dynamic, object oriented and general purpose. Like JS, it also offers lots of ways to do the same thing, enabling the developer to have a fair amount of stylistic autonomy (if you like that kind of thing).
Ruby has a fairly low learning curve, since it seems to have been designed with lots of sensibly-named, consistent and easy to use methods, and it also has no concept of asynchronicity out of the box, making code easier to reason about than JavaScript.
Here are a few things I've noticed which I think are pretty neat about Ruby, in comparison to JavaScript!
Being able to check memory location
Unlike in JavaScript, Ruby lets you inspect the location in memory of a value with the object_id
method:
For example, if we look at the object ID of 2 hashes:
a = {name: 'Harriet'}
b = {name: 'Heather'}
puts a.object_id # 6478367
puts b.object_id # 6471222
Those numbers are memory addresses. The actual addresses aren't that useful but it might help to see when you're dealing with two references to the same location in memory, or references to separate locations in memory.
I've never used this in a practical sense, but it was helpful when I wanted to explore the difference in how Strings and Symbols work in Ruby (see next section). There's no way in JavaScript to inspect where items live in memory, which has been annoying when I've been trying to demonstrate how JavaScript passes objects by reference, and primitives by value.
Symbols
In JavaScript, you have a few of ways of creating a string, the first two here being the most common:
let a = 'Hello world'
let b = "Hello world" // Functionally no different to using single quotes
let b = new String('Hello world') // Creates a String object
let c = `Hello world` // ES6 String Literal
In Ruby, there are also a few options:
a = 'Hello world'
b = "Hello world" # Double quotes allow for string interpolation & escape characters
c = String.new('Hello world')
d = String('Hello world')
In Ruby, by default, all Strings are types of String Objects, and as Objects they occupy different places in memory, even if the contents of two or more strings is the same. Potentially a bit wasteful, storing the exact same information twice over!
You can check this by looking at the object ID of 2 identical strings:
a = 'Hello world'
b = 'Hello world'
puts a.object_id # 6478367
puts b.object_id # 6471222
That’s where Symbols come in. A Symbol is created with a : at the beginning and means that any time the Symbol is used, it will reference the same value.
a = :hello
b = :hello
puts a.object_id # 1111708
puts b.object_id # 1111708
This works great for single words, but you can even turn a longer string into a Symbol and increase efficiency with the .to_sym
method:
a = 'Hello world'.to_sym
b = 'Hello world'.to_sym
puts a.object_id # 92880
puts b.object_id # 92880
I use symbols over strings wherever I can, not just when I know a value will be used again in a program! Why not, when it’s easy to do and there’s nothing to lose?
Simple loops ➰
In JavaScript, sometimes you just want to loop a set number of times. You don't care about the start point or the end point, as long as your code executes n times. However, you're forced to explicitly construct the conditions for iteration yourself, starting with i = 0 and defining when you want the loop to end:
for (let i = 0; i < 10; i++) {
// do stuff
}
In Ruby, you can simply do:
10.times do
# do stuff
end
It's a simple, less imperative way of executing code a set number of times.
Functions are stricter about arguments
I like that in Ruby, you get an error if you give a function the wrong number of arguments. It just speeds up the process of debugging your code.
def greet(name)
puts "Hello, #{name}"
end
greet()
# wrong number of arguments (given 0, expected 1)
You can also name your parameters, and if they're not passed, or you pass something unexpected, you'll get an error:
def greet(name:, city:)
puts "Hello, #{name} from #{city}"
end
greet(name: 'Harriet', city: 'Manchester')
greet(name: 'Harriet') # missing keyword: city
greet(name: 'Harriet', city: 'Mancheseter', age: 27) # unknown keyword: age
No function call parentheses
In JavaScript, you must use parentheses when calling a function, for example add(1, 2)
.
In Ruby, parentheses are generally optional, which can sometimes lead to Ruby that looks very natural language-y and easy to read. For example, a testing library can provide a to
method which, when used without parentheses, reads like this:
expect(result).to be_null
Although it can get a bit confusing if you've got multiple arguments. For example, is 5
the second argument to bar
, or the second argument to foo
? Without brackets it's not clear:
def foo(a, b)
puts "in foo #{a}, #{b}"
end
def bar(a)
12 + a
end
foo bar 55, 5 # wrong number of arguments (given 2, expected 1)
foo bar(55), 5 # Correct - 5 is the second argument to foo
Calling a function without parentheses also means we can do something like this:
def greet(name = 'Harriet')
puts "Hello, #{name}"
end
greet
See how just referring to the greet
method actually invokes it with no arguments? This is how Ruby implements getter methods on objects. When you call person.name
for example, name
is actually a method on that object, which retrieves the name
instance varaible. It's not simply an object property like in JavaScript.
One effect of parentheses-less method calls means we can't pass methods around as values, like we can in JavaScript. In JavaScript, we can do this:
function greet(name) {
console.log(`Hello, ${name}`);
}
const welcomer = greet;
welcomer('Harriet');
But in Ruby, trying to pass a reference to the method actually invokes it! So we end up with:
def greet(name = 'Harriet')
puts "Hello, #{name}"
end
welcome = greet # This line actually executes the greet function
welcome "Paula" # undefined method `welcome' for main:Object
Just one way to create Classes
In JavaScript there is not really a concept of true classes, at least not in the way people from truly Object Oriented languages would expect them to be. Instead we have a the prototype chain, and at least 4 different ways of creating objects with shared methods and behaviour. This is super confusing, so I really like that Ruby just offers the one way to do it!
Creating class methods, class variables, instance methods and instance variables is much more straightforward in Ruby:
class Person
attr_reader :name, :title
# Class variables
@@legs = 2
@@arms = 2
@@diet = 'omnivore'
def initialize(name, title)
# @name and @title are instance variables
@name = name
@title = title
end
# Instance method
def greet
puts "Good day, #{title} #{name}!"
end
# Class method
def self.describe
puts "A person is a #{@@legs}-legged, #{@@arms}-armed #{@@diet}"
end
end
jerry = Person.new('Jerry Jones', 'Mr')
jerry.greet
Person.describe
Implicit returns
In Ruby, the return
statement is optional, or can be used to return early from a function. If you omit it, the function will return the last evaluation.
def double(nums)
nums.map{ |n| n * 2 }
end
Metaprogramming
This one's quite a big a topic and I don't know it that well so I'm only going to touch on it briefly. Metaprogramming means a program being able to modify itself at runtime, based on the state of the program at that time.
Rails uses Metaprogramming to allow us to do something like this:
Book.find_by_ISBN("19742084746")
You defined the Book
class when you set up your Models, but nowhere did you defined the find_by_ISBN
method. Defining a find_by_x
for all your columns would be really tiresome; it's no wonder the Rails framework doesn't want to make you go to all that work. But Rails itself didn't add that method for you, either. How would Rails magically know what your Book instances needed a find_by_ISBN
method?
Instead, when Rails sees you trying to use the find_by_ISBN
method it will extract the ISBN
part and attempt to match it to a column in the database, and if successful, will execute some code to find your item based on the ISBN
column, responding as though find_by_ISBN
were an actual method that had been defined on Book instances.
This is just one example of what Metaprogramming can do!
Personally I think it's pretty cool, and once you know it exists, you begin seeing it "in the wild" all over the place in Ruby. It's the foundation stone for being able to create DSLs (Domain Specific Languages) like Rails and makes Ruby extremely flexible.