I know what you're thinking but I want to start by saying that I love Ruby. Ruby is the language that made programming fun again for me after losing interest for a number of years. Ruby revitalised my career and allowed me to grow into the senior software engineer that I am today. Ruby is a lovely language. I just want you to stop using it.
For me Ruby came along in 2008 when I was working as a systems administrator at a local computer science department. I had written code in a bunch of languages before, and even enjoyed some of it but I was never able to feel completely productive or in tune with the language the way I felt when I started using Ruby. I was working with another team member to build a system to reimage lab computers using a combination of Rails, PXELINUX and BitTorrent. It was truly mad science. Also it worked really well - but that's a story for another day.
There was a bunch of reasons I stuck with Ruby after that first exposure and eventually transitioned into being a professional Rubyist. Below are the reasons I'm moving on and I think you should too.
If you wanted to make web applications in 2008 then Rails was simply the way to go. Sure there were other choices, but there were not other choices which were mainstream, had a reasonable talent pool available and were economic.
Rails drove the post-GFC startup economy. Not just because of the hipsters but also because it was the only framework at the time that allowed you to be so productive so quickly and to pivot your app easily when your hipster CEO decided to switch your dog food subscription website into an organic hemp running shoe e-commerce site.
Back then "the job" was a fairly constrained problem set: Take something from the database, squeeze it between brackets and serve it to the user. Then take some user input and stuff it back in the database.
These days the domain of a web app has expanded to include websockets, single page apps, APIs with their own query languages, web assembly and a bunch of tech that falls under the designation of "cloud". Web apps today look and behave completely differently to their 2008 counterparts - unless you look at a Rails app. Rails apps are still shuffling data back and forwards between databases and brackets - only these days there are more kinds of brackets.
Ruby is a scripting language, meaning that the Ruby interpreter reads in source code, parses it and immediately starts evaluating it. There is no formal compile step. Runtime code defines (and redefines) constants, classes, methods and everything else. Runtime code can write more runtime code or modify or delete existing runtime code. In some cases the same class can have different behaviour depending on what lexical scope you're in. Ruby's semantics are so flexible that it was no wonder that Rails came along and told everyone their code had to fit into one of three folders.
When designing systems in Ruby you'll hear a lot about duck typing - the idea that you don't care what the type of an object is as long as it responds to the method you need. This is a way of having protocols without having to define them before hand, this makes sense because there is no "before hand", there's only runtime. In conventional cases it works quite well because most Ruby devs know that if you implement
each then you can become an enumerable.
What this tends to mean in practice is that dynamic typing leads to subtle bugs because the wrong object is in the wrong place at the wrong time. The most common occurrence of this the infamous
NoMethodError on NilClass. What this is saying is that you tried to call a method that doesn't exist on
nil but what it really means is that somewhere below you in the stack a method returned
nil instead of the thing you were expecting. It doesn't tell you where your bug is, just where the effects of it were finally intolerable to the system.
Because Ruby's semantics defy almost any kind of static analysis where a user of another language could add a type constraint or at least pattern match to avoid this situation the Ruby programmer is left with only one option: write a truly heroic number of unit tests in an effort to formalise what data is allowed to flow in and out of methods and objects and that it doesn't explode when the wrong input is given.
One other side effect of duck typing is the common use if backhanded type checking in the form of
object && object.method to verify that the collaborating object is at least truthy before calling a method on it. This is so common that Ruby 2.3 added the
&. safe navigation operator to turn this design problem into a virtue.
Let's get this out there first of all. Ruby is slow. Conventional wisdom is that Ruby is "fast enough" and that you can always throw more hardware at a problem. This is true to a certain extent, but most machines are multi-core these days and...
I don't consider myself as the threading guy, so I don't think I can make the right decision about the Actor library or the threading library.
Matz - creator of Ruby
...Ruby is also no good at concurrency. MRI has a global interpreter lock, which means that only one YARV instruction can be run at a time. There's a bunch of places where this can be worked around - like sleeping threads waiting for IO but fundamentally, if you want to do more than one thing at a time you need to run multiple interpreter processes.
Lastly, Ruby tends to use a lot of memory and it doesn't like giving it back to the operating system. There are ways to mitigate these problems to a certain extent, but most Rubyists are not aware of them or are simply used to the situation.
A lot of work has been put in by the Ruby core team over the last few years and I'll be the first to admit that Ruby 2.5 is much much faster than it used to be. Many would argue that it's fast enough. I would not.
As described above, even when running with multiple threads Ruby essentially runs a single interpreter with no built-in fault tolerance primitives so unhandled exceptions will take down the entire VM. There have been legendary efforts by the community at writing actor systems but ultimately those teams have given up and moved to languages who give them the tools they need to build reliability into their products (example Celluloid).
The Ruby (and especially Rails) community lead the way in terms of good tooling to help developers achieve results quickly. Tools like Rake, Bundler, RSpec and the Rails command line really did blaze a trail for developer experience, however other languages have caught up and even overtaken Ruby in their tooling game. Rust's cargo, Elixir's mix and Scala's sbt all combine these tools into a single command and go the extra step of tightly integrating them directly into the language. And it's not just languages that have upped their tool game but also client side frameworks, DevOps systems and cloud providers.
Ruby is blessed with a very large number of packages (or gems) which provide pretty much any kind of functionality you can think of. Not quite as many as that other language but I'd argue that on average they are of a much higher quality. Need an out-of-the-box authentication system? There's a gem for that. Need a massively overblown and architecturally questionable state machine that is weirdly tied to your database? There's a gem for that too.
Before Rubygems there really was only CPAN but no one needs to be reminded of that. Ruby's model of easily consumable and integrated packages paved the way and has been adopted by many many other languages, eg: crates, hex, CocoaPods and elm/packages.
irb is still my go to tool to quickly work something out and I'll still write little Ruby scripts to orchestrate little one-off jobs like moving files around or sucking down an API responses.
When we talk about Ruby it's hard to ignore Rails, so I guess the question is "what should I use Rails for in 2018"? If you have an app that meets the following criteria then I expect that Rails will still be a good match for you:
- is almost entirely database driven,
- renders pages on the server or generates JSON,
- can be mostly assembled out of existing gems,
- doesn't have complicated business logic,
- doesn't need to be fast,
- doesn't need to serve many people at once,
- won't ever change.
I'd still argue that you have better options though.