Marie Kondo became famous following her 2019 Netflix original appearance in which she popularized the practice of throwing away items which do not spark joy. Since then, sparking joy has become a generic description for anything that is worth doing or keeping. The stereotypically Japanese way of doing things, which is exemplified in lean manufacturing techniques popularized by Toyota, is replete with examples promoting a highly principled approach to planning and design.
Ruby, which also traces its origin to the land of the rising sun, owes its creation to Yukihiro Matsumoto, who wished to design a programming language that combined elements of functional, object-oriented, and imperative paradigms into a language that developers would enjoy writing.
Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away. - Matz
Ruby encourages good naming conventions
Most languages have a loose set of recommendations for style. Python encourages snake case for method names and pascal case for class names, JavaScript developers are encouraged to use camel case for all identifiers, meanwhile Kotlin in most IDE environments enforces camel case for identifiers to such an extent that using snake case results in build warnings. Similarly, Ruby encourages snake case for identifiers and pascal case for class names and module names.
Ruby prescribes additional recommendations for method names that make them more descriptive. Because Ruby encourages immutable constructs, methods that modify state should be labelled with an exclamation point, while methods that return booleans should be labelled with a question mark.
For example, sort
sorts the array and returns it, while sort!
modifies the
array in-place:
numbers = [5, 2, 5, 3, 4]
numbers.sort => [2, 3, 4, 5, 5]
numbers => [5, 2, 5, 3, 4]
numbers.sort! => [2, 3, 4, 5, 5]
numbers => [2, 3, 4, 5, 5]
Similarly, include?
ends with a question mark because it’s expected to return
a boolean value:
numbers = [5, 2, 5, 3, 4]
numbers.include? 3 => true
numbers.include? 9 => false
Ruby’s functional constructs make bugs harder to write
Ruby discourages mutating variables in-place. Mutability and side-effects are often necessary in the real world, but there are many situations where they can (and should) be avoided. Each variable you mutate creates a ‘parallel’ state you have to mentally trace while writing or reading code.
In Ruby, every method and operation seems to return something; even assigning variables returns something:
b = (a = 3) => 3
a => 3
b => 3
This fact means that methods and assignments can be chained together to produce very expressive and beautiful code:
# Get very odd number between 1 and 100 and sum them together
(1..100).filter(&:odd?).sum => 2500
In addition to this, Ruby has implicit returns and allows for calling methods like accessing variables. Implicit returns might at first glance seem like an avenue for producing more bugs, but in practice it encourages developers to avoid assigning superfluous variables and focus on maintaining coherent control flow. You might think calling methods like accessing variables could be problematic. After all, methods are not variables; they can have side-effects! That is true, but if you’re using immutable variables like you should, this will never be a problem.
Ruby has a method for everything
Ruby has a method for everything. This seems to be a disadvantage at first, since it gives Ruby a higher perceived learning curve, but that need not be the case. Ruby’s features come to you slowly as you write it, and you’ll find yourself missing them when you work in another language.
For example, I often need to slice long lists of items into batches, usually as preparation for parallel processing. In Ruby this is remarkably simple:
words = ["list", "of", "example", "words", "to", "be", "split"]
words.each_slice(3).to_a => [["list", "of", "example"], ["words", "to", "be"], ["split"]]
Consider the same problem in Python, a language that supposedly competes with
Ruby on language features. Python has no built-in method for doing this.
Python’s itertools
library does have an equivalent method called batched(p, n)
that accomplishes the same function as each_slice
, but this is only
available in Python 3.12. If like most people, you’re using a Python version
before that on an existing project, the only solution is to write your own with
a list comprehension or write a method that calls islice()
on the input as an
iterator until it’s consumed.
Ruby has superior data structures
Ruby, like other languages, supports hash maps between two arbitrary types. These structures are useful for mapping one set of values to another and run quickly because they use hash buckets for fast access times. As a result, they have become ubiquitous in modern languages, especially in Python where they are called dictionaries.
Ruby calls these structures hashes, and allows for two different types of keys. The traditional strategy of using a string as a key is supported:
string_hash = {
"key_one" => 3,
"key_to" => 55
}
string_hash["key_one"] => 3
But Ruby also supports symbols. Symbols are Ruby’s answer to C# enumerations, which map named constants to integers. This promotes speed because the overhead of hashing strings is no longer needed. Strings and symbols can be converted between each other with ease, and hash syntax has a quick shorthand for using them as keys:
# Strings and symbols are easily converted
"string_value".to_sym => :string_value
:symbol_value.to_s => "symbol_value"
symbol_hash = {
key_one: 3,
key_to: 55
}
symbol_hash[:key_one] => 3
Ruby encourages good codebase organization without being too opinionated
Like Python, Ruby has the concept of modules and module scope. Unlike Python, modules are defined explicitly and aren’t necessarily coupled with file names.
In Python, directory structure produces a module hierarchy:
my_project/
--> __init__.py
main.py
submodule_a/
--> __init__.py
helper.py
thing.py
Thus, to import something from the module hierarchy you need only use from submodule_a.helper import ClassName
.
Ruby uses a similar pattern for imports, but modules are defined hierarchically as well:
module MainModule
module SubmoduleA
class ClassName
end
end
end
Instantiating the class from a file that has imported the source with require
or require_relative
is as simple as:
MainModule::SubmoduleA::ClassName.new
Ruby doesn’t waste time with object-oriented formalities
Although Matz often sings the praises of object-oriented programming, he isn’t a sycophant. Ruby encourages the use of inheritance, composition, and classes to solve problems that are well-suited to them. Inheritance, when used correctly, can save code through polymorphism without compromising readability.
Ruby thus supports private methods and fields by default and allows them to be selectively exposed without the asinine Java/C# rituals of writing getters and setters:
class MyClass
attr_reader :readable
attr_accessor :accessible
def initialize(a, b)
@readable = a
@accessible = b
end
end
inst = MyClass.new 'can_change', 'can_read'
inst.readable = 'cannot change'
(irb):70:in `<main>': undefined method `readable=' for #<MyClass:0x00007858455c6438 @readable="can_read", @accessible="can_change"> (NoMethodError)
inst.accessible = 'can change'
# no issues
Designating private methods is as simple as using the keyword private
, which
marks every method below it as private.
Null safety
Kotlin fans often assert that Kotlin is a modern and safe language because it
supports null safety. Interpreted languages tend to crash when code attempts to
access a method or property of a null object. In Kotlin, unless a variable is
marked explicitly as nullable with ?
, attempting to assign null to it will
cause an immediate compilation failure.
Ruby 2.3 introduced null safety using &.
, which allows null safety even in
the complex chains of method calls endemic to Ruby’s design. What happens if
one method in the chain returns nil
? Without null safety, this throws an
exception, but using the &.
operator, we can avoid an exception and return
nil
and short-circuit the entire method chain.
def mess_up
nil
end
mess_up.sort
`<main>': undefined method `sort' for nil:NilClass (NoMethodError)
mess_up&.sort
=> nil
And many more
In addition to the features and traits I have already enumerated, Ruby has a wonderful standard library: