Sorbet is cool, but Crystal is smoother
The ability to enter Ruby in no time and create a blog in fifteen minutes led to the wave of startups prototyped fast, some of which have grown into million-dollar companies by now. However, incredibly fast development in the beginning leads to slower development cycles in the future. Every new feature requires more test coverage and that one inevitable thought:
Would it crash in production if I’ve missed something?
Over the past few years, the amazing Ruby community came to understanding that it needs type-safety and increased runtime performance.
Sorbet adds a kind of type safety to Ruby. However, it’s not the traditional type safety of a compiled language, which Sorbet developers state clearly on its website. Sorbet makes a developer’s life easier by denying to run a program on methods arguments mismatch upon static compilation. It also includes optional runtime extensions to check types while a program runs.
However, Sorbet doesn’t change the way the interpreter views the application code. It also does not make a program run faster by any means, as Sorbet is not responsible for any precompilation optimizations. In fact, a program utilizing Sorbet in runtime would run a bit slower.
A serious drawback of using Sorbet is inevitably worse syntax. It adds a lot of signatures, mandatory T::Sig extensions, magic comments (# typed: true) and separate .rbi files. This makes Ruby lose its beauty and expressiveness. It becomes more enterprise like Java, where safety and confidence are crucial.
Of course, using Sorbet is optional, but looks like the Ruby team has an intention to merge it into the core, meaning that sooner or later Ruby which we used to love will be no more. There will be no joy of sitting aside a lake observing its beauty, but heartless business requirement of confident deploys. It’s a bit pessimistic, of course, but I always valued the visual experience a programming language gives.
Sorbet seems like a great tool for existing Ruby applications with huge codebases where any change would require lots of attention (and tests) to not to break existing code. Type-checking would indeed increase the trust into deploys and make shipping easier. Therefore such a move is absolutely justified if you already have many LoC and/or developers. But…
Modern web applications are typically database wrapping APIs with growing number of conditions to be met before accessing the raw database entry. Each of these conditions bring additional complexity meaning more object allocations and calculations. In popular dynamic languages interpreters (i.e. MRI for Ruby), allocations and method calls are quite resource-intensive. That’s in contrary to compiled languages, where all types are known before the program is run.
Ruby 3x3 goal confirms that the community is aware of the performance issue, but still, even x3 speed increase wouldn’t make Ruby fast.
One of the main performance gains achieved with type-safe languages is that methods are called immediately because their argument types are already known. In dynamic languages these arguments must be checked in runtime.
Sorbet allows to declare a method’s arguments’ and return types. Indeed, if you try to call this function with a String, it would raise upon calling srb tc:
As I’ve mentioned above, Sorbet doesn’t change the way an interpreter views your program, therefore a and b would still be checked in runtime by MRI. That’s in addition to the runtime overhead Sorbet adds by itself!
Ruby is a dynamic programming language. Its line-by-line interpretators will never be fast enough to compare with compiled languages. JIT is a nice attempt to do so, but usually its performance gain is neglected by the amount of CPU load and RAM it needs to perform its just-in-time compilations.
Premature optimization is bad, you would say. However, sooner or later a business understands that Ruby became a bottleneck and even caching doesn’t help enough. Usually such a business hires a number of Go or Scala or another compiled language developers to rewrite critical parts of the codebase for increased performance. This requires resources to communicate between Ruby and another-language engineering teams, support multiple codebases and also multiple tool sets, which means additional expenses.
Even with Sorbet, the performance bottleneck is inevitable and it’s just postponing what’s to come. It’s not a silver bullet by any means. Despite of a number of Ruby developers you have — even with type-safety — you cannot outcome fundamental dynamic language flaws. The most basic operations like object allocations and calculations would still be slow and sequential in Ruby (remember, MRI has GIL).
But what if there was a language with syntax similar to Ruby, but statically-typed and incredibly fast? This would restrain technical debt and make the codebase sustainable for long-term development, still keeping the expressiveness. Imagine having C-like speeds but preserving the development joy and great shipping confidence. This would make it perfect choice both for new and existing applications.
There is such a language.
Crystal is a garbage-collected statically-typed compiled language with syntax heavily inspired by Ruby. It resembles Ruby so much that it is even possible to run some Ruby code with Crystal! However, it doesn’t have a goal to be able to compile any Ruby code.
Crystal uses an LLVM backend, which brings tons of precompilation optimizations, leading to superior performance and incredibly small resource footprint. All these gains preserve the familiar Ruby-like syntax you are already in love with:
The sum example mentioned above could be written in Crystal like this:
Crystal has a smart compiler, which does not require to explicitly declare types everywhere, but still, it’s fully type-safe. The example above could be rewritten as following:
Unlike with Sorbet, you don’t need to have an external tool to check the types. A Crystal program would never run if you’ve have type errors anywhere.
Crystal is fully OOP (everything is an object), it has native interfaces, stack-allocated structs, incredible concurrency model resembling goroutines; unions, generics, overloads and much more!
Crystal has growing ecosystem with most of the instruments needed for typical application development. These are some resources to start with:
- Official documentation —honestly one of the best language documentations ever, kudos to all its contributors!
- Crystal for Rubyists — the guide for seamless migration
- Programming Crystal — a real-life printed book
- Awesome Crystal — everything awesome about the language
Of course, Crystal already has some web frameworks to play with:
- Kemal — the first, grandfather web-framework inspired by Sinatra
- Amber — a community-maintained full-stack Rails-like web framework
- Lucky — a full-stack web framework sponsored by Thoughtbot
- Onyx — a full-stack web framework designed to be simple to start with yet to grow with a developers’ knowledge by yours truly
- Athena — an annotations-based web-framework
- … and many more
Among the other features Crystal has is extremely simple bindings to C libraries. Developing cross-platform applications and games, embedded software, manipulating videos and much more is already possible by writing a simple binding file in pure Crystal. Learn more in docs.
Of course, compiled languages are harder to learn than dynamic ones. However, Crystal is designed to have as smooth learning curve as possible. Once again, visit its docs to get started. Don’t forget a cup of tea for that!
Although the language has not been officially released yet (still 0.x), it’s already been used in production be many companies, including Manas, Yahoo, Nikola Motor, Rainforest, Diploid, NeuralLegion and more.
To close the arc, with Crystal you’d ship features fast and confidently during the whole project lifetime, not only in the beginning. Crystal allows to create long-term sustainable applications without sacrificing the joy of programming. The only thought that’s in your head would be:
Which feature is going to be implemented next?…
You can begin working on a new Crystal project from scratch, or adopt it incrementally in your existing application replacing critical parts. Find me, Vlad Faust, on Twitter, GitHub and vladfaust.com, if you need a hand.
P.S: Thanks to Ilya, Andrey(s), Yuri, Zac and Johannes for helping me on this writing. I appreciate that.