Some time ago I wrote a post highlighting some the Ruby features that I find to be its strong points. The list was by no means exhaustive. Such list would be really long, as I believe there are many things that Ruby got right.
However, nothing is perfect and so isn’t Ruby. Between all the powerful features, elegant syntax and clean object model, there are some elements of the language hidden, that are inefficient, confusing or simply ugly. In this post, being the follow-up to the aforementioned “Good parts”, I will focus on Ruby’s weak points. The list will be completely subjective and you may not agree with some of the points. Or perhaps there’s something you find annoying that I failed to mention. In any case, I’d love to hear your opinion in the comments section!
Performance
Ruby has a reputation of being a slow language. One reason of that is the simple fact that Ruby is a dynamic, interpreted language. If we compare it to statically compiled language, such as C++, we’ll find out that it is, indeed, slower in most cases. How much slower exactly? According to numerous benchmarks comparing performance of different programming languages, Ruby application can be up to two orders of magnitude slower than analogous program written in C++. It’s definitely enough to conclude that Ruby is not one of the most performant languages in the world, to say the least.
However, one might argue that it’s like comparing apples and oranges. It would be more fair to compare Ruby with another language of similar nature. A great candidate for such confrontation is Python. It’s a dynamically-typed, interpreted language, just as Ruby. In the distant, ancient times of Ruby 1.8, the faster of two languages was Python. The difference wasn’t nearly as huge as with C++, but it was there. However, the Release of version 1.9 of our favourite language brought a major speed improvement, as many sources can confirm (see: one, two, three. Ruby 2 and 2.1 brought further speed-up. isrubyfastyet.com allows you to see how increased Ruby speed affects performance of web applications written in Rails.
So, is the Ruby’s performance something you should worry about? The answer is: it depends. Ruby might not be your language of choice for implementing computationally complex algorithms or real-time systems. However, perhaps Ruby’s most common use case are web applications. For most of them, Ruby’s performance is more than enough. More often than not, you’ll get much bigger improvement in your application responsiveness by optimizing database queries, skillful use of caching, delegating time consuming operations to background processes etc. Of course writing your application with some other, faster language would, in many cases, allow you to reduce response time of your app by a couple of milliseconds. In most cases, however, slightly better performance is not worth losing the speed and simplicity of development which Ruby has to offer. The list of sites built using Ruby include twitter, airbnb, Github and Hulu. If Ruby performs good enough for them, then there’s a big chance it will also be a good choice for you.
Threading
This point could be a part of ‘Performance’, but I think it deserves a paragraph of its own.
In Ruby 1.8 we had so called “green threads”, which means they were scheduled by the interpreter, instead of using threading mechanisms of underlying operating system. Such threads are very portable - they work on any platform that the interpreter can be run on, since they don’t depend on operating system capabilities. They are also more lightweight than their native counterparts - they require less memory and are often faster to start. However, they have very serious limitations. First, they are limited to one processor. Second, a blocking I/O operation blocks all threads running within the process.
With the release of Ruby 1.9, threading mechanism has been switched to native threads. Every Ruby thread is mapped to an operating system thread. This allows Ruby programs to run on multiple processors. Blocking I/O operations no longer block other threads.
The performance of multithreaded applications is still limited, though. Ruby interpreter uses mechanism called GIL (Global Interpreter Lock) which allows only one thread to execute at a time, even if ran on multi-processor system.
It’s important to note, that above description refers to MRI. Some other Ruby interpreters, such as JRuby, which runs on top of Java Virtual Machine, or IronRuby, which makes use of Microsoft’s DLR (Dynamic Language Runtime) don’t use GIL, and are therefore free of this shortcoming.
and
, or
, not
I love Ruby for its elegant syntax and expressiveness. There are, however, some constructs in the language that - instead of making developer’s life easier and more pleasant - can be a source of confusion and make the code less readable.
One example of such problematic addition to the language’s syntax are
logical operators and
, or
, and not
. At first sight, they seem to
be just a syntactic sugar - an easier-to-read alternatives for more
traditional &&
, ||
and !
, respectively.
However, upon closer examination of their behaviour, it turns out that
they act differently than their counterparts.
The difference is that they have lower precedence, which may lead to a
lot of confusion if you try to use both operator types interchangeably.
They do, indeed, have their use. For example, have a look at the following code sample:
if user = User.find(user_id) and doc = Document.find(doc_id)
# code using 'user' and 'doc' variables
end
The above code wouldn’t work as expected with the ‘&&’ operator used in place of ‘and’. Let’s use even simpler example to picture it:
# using and operator
if var1 = 'AAA' and var2 = 'BBB'
puts "var1: #{var1}"
puts "var2: #{var2}"
end
=> var1: AAA
=> var2: BBB
# and now with &&
if var1 = 'AAA' && var2 = 'BBB'
puts "var1: #{var1}"
puts "var2: #{var2}"
end
=> var1: BBB
=> var2: BBB
To get expected result with &&
operator we need to add some
parentheses to
the mix:
if (var1 = 'AAA') && (var2 = 'BBB')
puts "var1: #{var1}"
puts "var2: #{var2}"
end
=> var1: AAA
=> var2: BBB
The above example clearly shows, that and
, or
and not
operators
can be quite useful and help us in writing cleaner code. However, I’m
not really convinced that these pros overweight the disadvantage of
making the code more confusing. It can be seen as a violation of POLS
(Principle Of Least Surprise).
As a sidenote, the ‘english’ operators originate in Perl. You can read more about their history in Preston Lee’s blog post.
BEGIN
and END
blocks
These also came to Ruby from Perl, although their
real origin is in awk.
BEGIN
and END
keywords allow you to define blocks of code that will
be executed as soon as the program starts and right before it exits,
respectively. See the example:
END {
puts "inside END block..."
}
puts "Hello!"
BEGIN {
puts "inside BEGIN block..."
}
# => inside BEGIN block...
# => Hello!
# => inside END block...
I have yet to see a situation where these blocks would be useful in a real life Ruby application. Not only they are useless, they affect normal flow of application, making it more difficult to understand and debug.
Global variables
It’s widely accepted opinion that global variables are evil. They make software harder to read and understand. They are accessible from every place in the code, which makes it impossible to properly isolate units of code. They increase the risk of variable name clashes. The list could go on and on.
Some languages don’t have global variables at all. Ruby is not one of
these languages. Not only it makes it possible to use them,
it has a set of built-in global variables. A couple of examples of such
variables are $@
, $?
, $1, $2, $3 etc...
. These definitely don’t
have
the most self-explaining names I could think of, which is another
problem.
What are they used for? A fair number of these variables is related to
regular expressions matching, for example:
- $& - text matched by regex
- $` - text preceding the match
- $\’ - text following the match
- $1, $2, $3 … $n - text matched by nth group
Another examples include:
- $* - alias of ARGV, read-only
- $$ - PID of current process
- $? - exit status of the last terminated process
Tutorialspoint contains more complete list of Ruby’s predefined global variables.
The global variables and their cryptic names are borrowed from… yes,
you guessed it right. It’s another perlism.
Luckily, Ruby is shipped with English
library, which adds much more
intention-revealing names to built-in global variables. The list of
aliases can be found in
ruby-doc.
To use the aliases, you must remember to require the library in your
code:
require "English"
"abc" =~ /.{3}/
puts $MATCH
=> "abc"
That’s definitely more readable, but we’re still using global variables.
Similar result can be achieved with match
method:
m = "abc".match(/.{3}/)
puts m[0]
=> "abc"
Encodings
Before version 1.9 Ruby’s support for multilingualization (or m17n for short) was poor, to say the least. By default Ruby supported only ASCII encoded strings and assumed that every character is encoded using exactly one byte. It was possible to add support for other encodings, such as UTF-8, but working with them was quite painful.
Since the release of aforementioned 1.9 version things have improved significantly. Contrary to most languages, which have one global encoding for all strings, Ruby uses per-string encodings.
Full list of supported encodings can be listed in the following way:
Encoding.list
=> [#<Encoding:ASCII-8BIT>,
#<Encoding:UTF-8>,
#<Encoding:US-ASCII>,
#<Encoding:UTF-16BE (autoload)>,
#<Encoding:UTF-16LE (autoload)>,
#<Encoding:UTF-32BE (autoload)>,
#<Encoding:UTF-32LE (autoload)>,
#<Encoding:UTF-16 (dummy) (autoload)>,
#<Encoding:UTF-32 (dummy) (autoload)>,
#<Encoding:UTF8-MAC>,
Encoding.list.count
=> 100
Similarly to strings, regular expressions also have their encodings.
Source files have their encodings as well. To set the desired encoding
you need to add a comment on the first or second line of the file,
containing the world encoding
followed by the colon and the encoding.
For example beginning of UTF-8 encoded source file can look like this:
#!/usr/bin/env ruby
# encoding: utf-8
Source code encoding allows you to do things like using unicode characters in string literals or variable names.
Two remaining types of encoding are external encoding, responsible for dealing with input-output objects and internal encoding, which is used, well, internally. Let’s see it in action.
First, we’ll create a text file with some funky polish characters, using iso-8859-2 encoding:
ąęć
Now we’ll open the file.
File.open("iso-8859-2.txt", "r") do |f|
puts f.external_encoding
puts f.read # content
puts f.read.encoding # internal encoding
end
# UTF-8
# ���
# UTF-8
Well, it didn’t work. The file was interpreted as UTF-8. Let’s try explicitly specifying the external encoding. We can do it by appending the encoding to mode argument:
File.open("iso-8859-2.txt", "r:iso-8859-2") do |f|
puts f.external_encoding
puts f.read # content
puts f.read.encoding # internal encoding
end
# ISO-8859-2
# ���
# ISO-8859-2
This time the file was interpreted as iso-8859-2, but we still can’t see the characters. It’s because our console expects UTF-8 encoded characters. We can fix the problem, by specifying the internal encoding right after the external one:
File.open("iso-8859-2.txt", "r:iso-8859-2:utf-8") do |f|
puts f.external_encoding
puts f.read # content
puts f.read.encoding # internal encoding
end
# ISO-8859-2
# ąęć
# UTF-8
Another possible issue with formatting might happen when you try to match string in one encoding against regular expression in another encoding.
The multitude of encodings in Ruby can sometimes be a source of headache. To avoid potential problems you should use unicode for everything.
Summary
Ruby is not perfect. Nothing in the world is. It has its bad parts and it’s important to be aware of them. This way we can make conscious decisions about whether it’s a right tool for the job or what features of it we should use and which ones should be avoided. Do you have some thoughts on this topic or would like to add something to the list? Please leave a comment!
Post by Kamil Bielawski
Kamil has been working with AmberBit since 2012, and is an expert on JavaScript and Ruby, recently working with Elixir code as well.