Ruby, the Python we deserved
My first "real" programming language, if we don't count Matlab and Mathematica (today, Wolfram language) I used in school while majoring chemistry, was Ruby, which is why I guess have I a soft spot for it even today.
I'm also well aware there is no real contest any more between Ruby and Python, the latter having taken over the programming world by storm, while the former is diminishing by the day, sadly.
Yet I still think Ruby is the better dynamic, "scripting", programming language.
The Python we deserved, but not going to get.
Yeah, well, you know, that's just like YOUR opinion, man
Yes, Dude, it is. Not too surprising as this is my blog. 🤪
Of course every opinion here is mine, and only mine. You can agree, or disagree, it's your right.
I just hope whether you're a Pythonista, a Rubyist, or a Gopher (FSM have mercy on your soul), you think about what I'm saying before you'll dismiss me and my writing as nonsense.
Objects all the way down, consistently
The most glaring, not sole, but definitely the poster child of Python's wrong design is the ambiguity between its pronounced "Everything is an object" paradigm, and its implementation, especially with regard to built-in functions.
Take for example the function to get an iterable object's length, of which the most common is an array, or list as it's known in Python (in itself a bad decision: a list in computer science has come to denote a singly-linked list. Python's lists are what other languages call array, and for a good reason: they denote a contiguous memory section.) The Python code is:
myLst = [1, 2, 3, 4, 5]
len(myLst)
This begs the question, "why?!?!"
Especially given that other list methods, e.g., appending, are accessed in the "dot-method" syntax, i.e., myLst.append(10)
, and are defined on the list class.
In contrast, in Ruby:
myArr = [1, 2, 3, 4, 5]
myArr.length # Can also use `myArr.size`, and even `myArr.count` (without any arguments to the `count` method)
The answer to the question "why?" is somewhat interesting.
In Python everything is indeed an object, lists included, and every object has its own methods.
There are, however, certain generic functions, not methods, that are not defined on any specific class.
Because those functions may be used on many data structures, of differing types, it was decided to group them inside a special dictionary: __builtins__
.
When a function is called that is not in any other scope, the Python interpreter looks those functions in that special dictionary, and if it is indeed there, the built-in function being referenced by the key is called.
len()
is one such function, and any object that implements the __len__
"magic method", as lists do, can call it (which, for lists, is exactly what the magic method does!)
Ruby, on the other hand, has its roots firmly in the Smalltalk programming language - the original language to coin such term as "class", "object", and "object-oriented"!
A core tenant of Smalltalk was how it defined objects: a data container that can only communicate with its environment via message passing. When an object receives a message it checks against its own finite list of known messages. If the incoming message is on its list, it carries out the corresponding action.
(Smalltalk's messages are not the same as today's methods since they're external to the object, and are more in tune with modern actor frameworks, but the concept is valid.)
It's clear, therefore, that in Ruby, length
(or size
, or count
) are methods on the array object. Also on, for example, the string object too!
Each class that needs its derived objects to return their size implements the appropriate methods, in a way that's correct for it.
There's no one, central, generic, function because that would mean that an array object wouldn't know how to respond to a length
method call passed to it!
The difference is less pronounced in, for example, mathematics where addition in both languages is implemented as a method on the corresponding numeric class with the +
operator being syntactic sugar over a method call.
For example in Python:
3 + 5 # => (3).__add__(5), the `add` methodn is implemented on the int class
3.0 + 5.0 # => (3.0).__add__(5.0), the `add` method is implemented, seperately, on the float class, too!
And in Ruby:
3 + 5 # => 3.+(5), `+` is the **actual** method name, implemented on the Integer class
3.0 + 5.0 # => 3.0.+(5.0), `+` is the **actual** method name, implemented, seperately, on the Float class, too!
I feel like Ruby's way of implementing object-orientation, rather Smalltalk's way, is better: whenever you "dot into" an object in your IDE you get the full list of what the object is capable of handling. No "magic" functions hidden in a global scope to have to guess.
Truly, "What You See Is What You Get" (WYSIWYG).
"Come up and see me, make me smile"
One of Matz's (Dr. Yukihiro Matsumoto, Ruby's creator) stated goals in inventing Ruby was to "optimized for programmer's happiness", what now days is known as "Developer Experience" (DX).
To that end, Ruby has, from inception, been a language more concerned with convention over anything else: if the community figured out that doing some task is best achieved in a particular way, that way became the norm. Matz never tried enforcing his opinion on the shaping of the language and its use.
More over, even if a certain way did become convention, Ruby almost always offers more ways of getting things done, as can be seen in the previous array length example: there are 3 such ways. Granted, two are simply an alias of each other, while the third is a bastardization of a different method all together, but there are options!
Another, complementary, goal of Ruby was "the principle of least surprise" (AKA "principle of least astonishment"): for a skilled developer in Ruby, there should be no code, no behavior, no quirk, that makes the developer go "huh?!". Once someone becomes acquainted with Ruby, coding in it should feel streamlined because one can reason about the code before even coding in a predictable way!
(Contrast that notion with, say, JavaScript, and nowadays TypeScript too, as can be seen in the now classic WAT video.)
Python's creator, Dr. Guido van Rossum, had another idea about what constitutes developer's happiness, and in Python we have "The Zen of Python" - a set of 19 dictates how Python code should be laid out and coded, that has some very strict ideas.
The Zen of Python was considered such an important part of the language's design that it was made into the 20th official PEP - Python Enchancment Proposal, the official way the community can suggest improvements to the language to van Rossum, who takes a very active charge of his creation's evolution, being its BDFL - benevolent dictator for life.
The Zen of Python is so central to the language that in the Python REPL you can view it by typing import this
.
Personally I prefer Ruby's convention-by-community over Python's "there should be one, preferably only one, obvious way of doing things".
Coding is a creative process as much as it is a technical endeavor. Ruby encourages freedom and exploration, Python leans toward strict correctness.
I see what you mean
One complaint anybody who ever coded in Java has about it is how verbose it is.
Well, neither Ruby nor Python are as verbose as Java, thank the FSM, actually feeling more dynamic while being on the JVM was one design goal of the Groovy programming language, but between them there's a clear winner for succinctness.
(Yes, I am well aware of the irony, me talking about succinctness. I like it in my programming languages, not in my own writing. 🤣)
Not so much about LoCs, but rather about readability, a few examples will clarify the subject.
We'll start off with something simple: read in a text file, and print the 3 most frequent words in it.
In Python:
counts = {}
with open("input.txt") as f:
for word in re.findall(r'\w+', f.read()):
word = word.lower() # Normalize for sotring and getting from the dictionary
counts[word] = counts.get(word, 0) + 1
top3 = sorted(counts.items(), key=lambda x: -x[1])[:3]
for word, count in top3:
print(f"{word}: {count}")
And Ruby:
counts = Hash.new(0)
# Ruby's `File.read` closes the file once out of scope, as does Python's `with`, but that's external to the file opening: `open(fileName)`
File.read("input.txt").scan(/\w+/).each { |w| counts[w.downcase] += 1 } # `w.downcase` is the normalizer as in the Python example`
counts.sort_by { |_, v| -v }[0,3].each { |w, c| puts "#{w}: #{c}" }
By the way, did you catch how in Ruby the last expression is returned by default? No need to remember to call return
... or bang your head in frustration, trying to debug the code, figuring out why our function returns None
.
Another all too common example: initializing a class constructor with instance variables.
In Python:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
And Ruby:
class Person
def initialize(name, age)
@name = name
@age = age
end
end
While one may comment on Ruby's use of def...end
, I like its human-readable initialize
over the magic method __init__
, and the fact that the @
is an instance variable definition, making short change of having to reference self
both in the constructor's parameters, and in assigning them values.
And, lastly, one more example: given an array of JSON-like objects, find those who are over 18, and capitalize their name.
In Python:
people = [{"name": "alice", "age": 17}, {"name": "bob", "age": 22}, {"name": "carol", "age": 19}]
adults = []
for p in people:
if p["age"] > 18:
adults.append(p["name"].capitalize())
print(adults)
And Ruby:
people = [{name: "alice", age: 17}, {name: "bob", age: 22}, {name: "carol", age: 19}]
adults = people.select { |p| p[:age] > 18 }.map { |p| p[:name].capitalize }
puts adults
Notice also that Ruby supports the notion of symbols as keys in dictionaries, so no need to quote the keys, unlike in JSON, and the keys aren't strings, but symbols which are singletons, making hashing them a touch faster than Python as they're placed in Ruby's symbols table on creation.
Also, visible in the above examples is Ruby's excellent block syntax for anonymous functions: blocks can be chained together to form a compound function, something simply impossible with Python's lambda
expressions.
Block can also be written in a line-by-line fashion, for example, for readability:
people.select do |p|
p[:age] > 18
end
Of course, this format does not compose.
A Functional (Programming) language
Ever since I learned of the Functional Programming paradigm I started detesting the Object-Oriented paradigm (as can be seen by my other post series on transforming code from Elixir, an FP language, to F#, another FP language.)
Aside from such attributes as immutability by default, FP languages have one more trait that makes them a treat to work with, it's in the name: they're functional. They consider functions as first-class citizens, making passing them as arguments to other functions, getting them back as a result, binding them to a value, etc. as easy as it gets.
Now, true, Python does allow some FP even at its core, without 3rd-party libraries, chief example of which is the afore mentioned lambda expressions. But that's pretty much the extent of things. A Python list, for example, doesn't have built-in, core library, facilities to map over its elements, filter them, etc.
Ruby, on the other hand, as we saw before, does. Out-of-the-box.
But Ruby takes it one step further with its Procs
, shorthand for procedures: blocks of code, anonymized functions, that can be bound to a variable and passed as arguments to other functions (that can then call on the procedure using the keyword yield
):
# First a simple example, directly calling a `Proc`, also showing the `.select` (AKA `.filter`) functionality of arrays
isEven? = Proc.new {|n| n.even?}
evenNums = [1, 2, 3, 4, 5].select(&isEven?)
# More involved: the `Proc` is passed to a **method** (here on the `Global` object as an argument)
def processNums(num)
nums.each do |n|
yield n # This calls the `Proc`
end
end
isEven? = Proc.new {|n| n.even?}
processNums([1, 2, 3, 4], &isEven?) # `Proc` pass explicitly, even though not in function's signature, because function expects it!
# We don't like signatures to "lie". Let's fix that
def processNums(nums, proc)
numbers.each do |n|
proc.call(n)
end
end
isEven? = Proc.new {|n| n.even?}
processNums([1, 2, 3, 4], isEven?)
It was actually in Ruby that I first grasped the power of Functional Programming, even though at the time I didn't know that's what it's called.
Ruby is a very much OO language, as we saw in an earlier section, but it always had, since its beginning, a strong FP streak to it which amplifies the joy of its developers, in line with its design goal.
Eat my dust
And now we should probably talk about performance, and, as much as I don't like admitting it, performance-wise, Python always had, still does, and probably always will have, the upper-hand.
Of course, if hyper-performance is an issue, neither language is a good fit, as they're both interpreted languages that require runtime compilation (i.e., JIT compilation) via their respective virtual machines.
But, assuming a subjectively "reasonable" performance is sufficient, yeah... Python usually beats Ruby by a factor of x2-3.
(I'm referring to the canonical implementations of each: CPython for Python and MRI for Ruby.
Experimental compilers such as PyPy for Python and TruffleRuby for Ruby are cool, but are not mainstream, and in that case, TruffleRuby, running on the GraalVM comes very close to native speeds!)
The way of the world
Ruby just got a bad deal, and its community missed out on opportunities.
Originating in Japan in a time when commercial internet was just budding, and being created by a reserved, soft-spoken Japanese man, in line with his culture and heritage, Ruby was not well positioned to take over the world by storm. It didn't help that for a very long time its official documentation were only in Japanese, either.
Contrast that with Python being developed by a Dutch with no reservations about promoting his creation, in the middle of Europe at a time when a good, succinct, relatively performant alternative to Java, itself an infant back then, not the performance powerhouse it is today, was sought for.
Ruby's community also missed out: after creating Ruby on Rails, which for a time dominated the web framework scene the community rested on its laurels, oblivious to how data science was picking up steam.
Python's community, on the other hand, saw clear and started creating multiple, competing DS libraries, and in the process pushing each library to become better, until the efforts were consolidated into a single DS stack that we know today (e.g., NumPy, Pandas, SciKit-learn, Numba, Matplotlib, PyTorch, and others.)
Python's grasp on the DS community is so strong today that even specialized languages for doing DS like Julia and R are losing the fight to Python, if they ever stood a chance to begin with.
As Python is rising, for DS, for web frameworks, for backend business logic, Ruby is declining.
That's just the way of the world: bad luck, bad timing, and yes, bad decision-making make for the suboptimal alternative taking the lead. We've seen it happening so many times, what's another one?
Personally in my hobby-coding and side projects, I've moved to Functional Programming and never looked back.
But if I was asked to start a new, greenfield, project today and use one of the more orthodox languages, I wouldn't take JS, nor TS, not Go (yeccch!), and no, not Python either.
I'd vote for Ruby knowing it will do the project proud, and give its developers joy.
Top comments (0)