Backend Development
The Crystal Language: A Promising Newcomer
A brief look into the new and promising Crystal programming language.
Historically, when choosing a language for a project, it seemed that one of the major questions you had to ask yourself was: Do I want it to run fast or do I want it to be easy and quick to write? In the first category you have languages like C, C++, and Java, and in the second, are languages like Ruby, Python, JavaScript, and PHP.
In recent years, a handful of languages have emerged that are as fast other compiled languages with the succinct syntax you would expect from a modern language—examples being Swift, Go, Elixir, and Crystal. Many have heard of Swift and Go, and Elixir is built on the Erlang’s BEAM VM for high scaling systems, but Crystal is an emerging language that is still relatively unknown.
Crystal, like Swift, is built on the LLVM. By taking advantage of LLVM, Crystal is able to achieve speeds faster than Go in many benchmarks—thanks to years worth of optimizations built into the compiler. However, what sets Crystal apart is its ability to maintain its blazing fast speed, while achieving the less verbose syntax you'd find in a scripting language. In fact, Crystal’s de facto mantra is “Fast as C, slick as Ruby”.
Look ma', no boilerplate ("Hello, world" in Crystal)
puts “hello, world!”
How does Crystal stack up to other languages?
I’m sure you want to see how Crystal stacks up to some other popular languages. I'll use Kemal to build a simple web API and compare it to similar minimal frameworks in other languages, namely Gin (Go), Express (Node.js), Sinatra (Ruby), and Flask (Python).
Click here for Kemal installation instructions.
Crystal:
require "kemal" logging false get "/" do |ctx| ctx.response.content_type = "text/plain" "Hello, World!" end Kemal.run
Ruby:
require "sinatra" set :logging, false get "/" do content_type "text/plain" "Hello, World!" end
Go:
package main import "gopkg.in/gin-gonic/gin.v1" func main() { gin.SetMode(gin.ReleaseMode) r := gin.New() r.GET("/", func(c *gin.Context) { c.String(200, "Hello, World!") }) r.Run() }
Node.js:
const express = require('express'); const app = express(); app.get('/', (req, res) => { res.set('Content-Type', 'text/plain').send('Hello, World!').end(); }); app.listen(3000, () => { console.log('listening on port 3000...'); });
Python:
from flask import Flask import logging app = Flask(__name__) log = logging.getLogger('werkzeug') log.disabled = True @app.route("/") def hello(): return "Hello, World!", 200, {'Content-Type': 'text/plain'} app.run()
Results:
Language | Python (Flask) | Ruby (Sinatra) | Node.js (Express) | Crystal (Kemal) | Go (Gin) |
Requests/Sec | 548 | 4,885 | 13,134 | 98,991 | 112,166 |
Lang Version | 3.6.0 | 2.3.3 | 7.3.0 | 0.20.3 | 1.7.4 |
Machine Specs | iMac macOS 10.12.2 (Sierra) / 3.2 GHz Intel Core i5 / 16 GB RAM |
In this example you can see Go just outperforming Crystal, but with more complex operations we would see the LLVM compiler really shine. Also, I would argue that the Crystal code is more readable than Go.
Neat Features
The String Builder
In most scripting languages, there is no concept of a string stream that you'd find in lower-level languages. For example, in many other languages there is not a more performant way of doing something like `"age: " + age.toString()`. The `age.toString()` expression would create a new instance of `String` in heap memory and then append the two strings together. The same thing in Crystal would look like `"age: #{age.to_s}"`.
But there is a more performant way of doing it in Crystal. You can use the string builder:
str = String.build do |io| io << "age: " << age end
A guide on Crystal performance shows this as being 4.75x faster than concatenation.
Functional Methods for Data Structures
Taking a page out of Ruby's book, Crystal has a very complete set of functional methods for arrays, hashes, tuples, etc. Personally, I never like writing a `for` or `for ... in` loop (which you'll never see in Crystal) so this is how you tend to interface with most data structures.
For example, to check if any element in an array is greater than `10`, you would do something like:
[1, 2, 3, 4].any? {|num| num > 10}
These methods can be chained together for some powerful functionality. This example will square each number and then check if any element in an array is greater than `10`:
[1, 2, 3, 4].map {|num| num * num}.any? {|num| num > 10}
Many of these functional type methods can be found in the Enumerable Module.
Built In Tooling
A major differentiator from most scripting languages are the built-in opinionated tools that come bundled with Crystal, namely:
- Application Scaffold Generator: `crystal init app my_app`
- This generates a basic app directory structure which standardizes the structure across Crystal projects.
- Code Formatter: `crystal tool format`
- This will modify spacing, parenthesis usage, indentation etc. across the codebase.
- Documentation Generator: `crystal doc`
- A standardized doc framework. The Crystal API docs are built using this tool.
- Spec (Testing) framework: `crystal spec`
- A generic testing framework.
Growing Pains
Crystal is still in early development so there is still some work to be done. It’s garbage collection engine is still immature (they plan to implement a more appropriate and custom garbage collector), it doesn’t yet support parallelism, it doesn't run on Windows (for now, you can use a software container like Docker for Windows). However, these concerns are being actively addressed by the core team in their new years resolution to get Crystal to v1.0.
Despite the baggage that a new language is bound to inherit, Crystal is still one of the best languages for running single threaded processes due to its high speed, low memory consumption, and the refreshingly terse, boilerplate-free syntax.
Personally, I believe the biggest pain points for this young language are:
- The lack of mature libraries and tooling
- No centralized dependency registry
- Breaking changes to core APIs causing dependency incompatibilities
The Lack of Mature Libraries and Tooling
Crystal is not like Ruby, Python, or PHP in that it doesn't have a rich collection of ready made libraries for most problems you will have. Do you want to parse an international phone number? Build it yourself. Want to access serial ports on an Arduino or a Raspberry Pi? Do it the hard way. I predict that after Crystal's APIs solidify that the libraries will catch up.
If you’d like to contribute something to the language, you don’t have to wait for 1.0. It’s very easy to make a difference—especially at these early stages. Within a month of experimenting with it I was able to get a contribution to the core doc parser. I also noticed that there wasn't a library that would restart an application when a file changes during development, so I made a tool in my free time.
No Centralized Dependency Registry
A lack of a centralized library registry means that there is not a streamlined way of installing dependencies (at least without copying and pasting a couple lines into a config file). You have to manually put them in the shard.yml file. This is not as convenient as Ruby’s `gem install x` or Node’s `npm install y`.
Breaking Changes to Core APIs Causing Dependency Incompatibilities
Part of the risk of adopting an early language are the regular breaking changes. Its exciting to constantly get new features that increase productivity or performance, but having to report issues to library maintainers that their library doesn't work with the latest version of Crystal is a recipe for stress.
Why Crystal?
As of now, Crystal is not quite ready for enterprise software development. However, Crystal's purpose may not be to solve a completely technical pain-point. Inspired by Ruby, it appeals to the diverse human tastes and preferences. Sure, Go seems like a more logical choice for a handful of reasons, but if you want something that has comparable performance to Go with a more succinct, human readable syntax and a more intimate community, then Crystal is a strong candidate.
Open source projects are started by developers to fulfill a personal need—to "scratch an itch". Go was initially created as an alternative to lower-level languages, whereas Crystal was created as an alternative to higher-level languages, both yielding a similar result. I enjoy writing in Crystal and will continue to look for any chance to use it.