An Introduction to Crystal: Fast as C, Slick as Ruby

Written by: Serdar Doğruyol

I'm a Rubyist. I love Ruby, the community, the productivity, and so many other things about it. For more than four years now, I've written Ruby professionally, and I'd really like to keep it that way. But I'm also aware that languages and tools are destined to be replaced.

Ruby is awesome, but it is not necessarily known for its speed. Sometimes it's not the right tool for demanding applications. And we have to wait a while for Ruby 3x3 to happen.

So I'm gonna ask you a question:

To be honest, I've always dreamed of something like that and wondered why it didn't exist. Then I found Crystal. I still remember it clearly: It was July 2015, I was reading /r/programming, and I saw something like "Crystal: Fast as C, Slick as Ruby."

So I went to the site, downloaded Crystal, and ran my first Crystal (actually Ruby) program:

crystal hello.rb
p "Hello World"

And it worked!

Hello World

The happiest Hello World of all times.

Enter Crystal

Let's take a closer look at some of Crystal's goals:

  • Have a syntax similar to Ruby (but compatibility with Ruby is not a goal).

  • Statically type-checked but without having to specify the type of variables or method arguments.

  • Have compile-time evaluation and generation of code, to avoid boilerplate code.

  • Compile to efficient native code.

There are some important points that we need to underline here. First and most important:

Have a syntax similar to Ruby (but compatibility with Ruby is not a goal).

This is pretty self explanatory. Crystal is not Ruby. It can't run Rails.

Statically type-checked but without having to specify the type of variables or method arguments.

Unlike Ruby, Crystal is a typed language, but most of the time it's not required to specify types. Take this, for example:

def greet(name, age)
  "I'm #{name}, #{age} years old."
end
greet "Serdar", 27 # I'm Serdar, 27 years old.

So when do we use types, and what they are useful for?

def add(x : Number, y : Number)
  x + y
end
# Ok
add 1, 2 # Ok
# Error: no overload matches 'add' with types Bool, Bool
add true, false

Great, this is a compile-time error. We restrict the method to only accept the types of x,y as a Number. In Ruby, this would be a runtime error, a.k.a., a disaster. Yay for Crystal!

Have compile-time evaluation and generation of code, to avoid boilerplate code.

Macros, anyone? Ruby is famous for its metaprogramming capabilities. Crystal uses macros to achieve that while reducing boilerplate code. This example is taken from Kemal, an awesome web framework for Crystal.

HTTP_METHODS = %w(get post put patch delete options)
{% for method in HTTP_METHODS %}
  def {{method.id}}(path, &block : HTTP::Server::Context -> _)
   Kemal::RouteHandler::INSTANCE.add_route({{method}}.upcase, path, &block)
  end
{% end %}

Here's how the DSL declaration is done in Kemal, looping through the HTTP_METHODS array to define a method for each HTTP verb. By the way, macros are evaluated at compile-time, meaning that they have no performance penalty.

Compile to efficient native code.

Crystal is a compiled language. I'm not gonna dive into the advantages of having a compiler, but I can easily say that it gives you a lot of optimizations for free. In addition, when a Crystal program is compiled, it's an efficient single file, native code. It's super convenient and easy to run/deploy.

Here's how you compile your Crystal program.

crystal build program.cr

it produces a single, executable, native binary with the same name that you can run with:

./program

Awesome!

Crystal's Fantastic Standard Library

Crystal comes with a great standard library and tools. It has all the stuff you need to build modern applications. CSV, YAML, JSON, HTTP, and even WebSocket are bundled with Crystal itself, making it super simple to start building something.

Need a web server? No problem!

# server.cr
require "http/server"
server = HTTP::Server.new(8080) do |context|
  context.response.content_type = "text/plain"
  context.response.print "Hello world, got #{context.request.path}!"
end
puts "Listening on http://0.0.0.0:8080"
server.listen

It's just 5 LOC to build a functional web server: crystal server.cr. Go to localhost:8080.

The crystal command itself is also really useful. It even has a built-in code formatter that you can use: crystal tool format your_app.cr.

The most amazing command is the crystal play. It's basically a playground to quickly run some Crystal code and get instant feedback. Just run it and go to localhost:8080.

Crystal's Performance

Crystal has a unique goal of being as slick as Ruby but with performance.

I love benchmarks, but remember, benchmarks should be taken with a grain of salt.

This is a naive Fibonacci implementation for Crystal (it's also valid Ruby):

# fib.cr
def fib(n)
  if n <= 1
    1
  else
    fib(n - 1) + fib(n - 2)
  end
end
puts fib(42)

Let's run it and see how long it takes!

time crystal fib.cr
433494437
crystal fib.cr  2.45s user 0.33s system 98% cpu 2.833 total

Since this is also valid Ruby, let's run it with Ruby this time

time ruby fib.cr
433494437
ruby fib.cr  38.49s user 0.12s system 99% cpu 38.718 total

Crystal took 2.833 seconds to complete. Ruby took 38.718 seconds to complete. Pretty cool. We get 20x performance for free. What if we compile our program with optimizations turned on?

crystal build --release fib.cr
time ./fib
433494437
./fib  1.11s user 0.00s system 99% cpu 1.113 total

1.113 seconds. Now we're nearly 35 times faster than Ruby. Isn't that cool? This is what I am talking about! The Crystal compiler uses LLVM to do really good optimizations.

If you're really into benchmarks (like I am), you should check out the crystal-benchmarks-game repository.

Remember: The cake is a lie, and so are benchmarks. You won't have 35x increase in performance all the time, but you can expect 5x or more in complex applications, more if it's CPU intensive.

Concurrency in Crystal

In Crystal, we use the keyword spawn to make something work in the background (a.k.a., async) without blocking the main execution. To achieve this, spawn creates a lightweight thread called Fiber. Fibers are very cheap to create, and their execution is managed internally by the process. You can easily create tens of thousands of Fibers on a single core.

Okay, we can use spawn to make stuff work in the background, but how do we send/receive something from a Fiber? That’s where Channels come into play. If you're familiar with Channels from Go, then you'll feel right at home.

Fibers can execute and keep sending messages through the Channels. Execution control is yielded to whoever is expecting to receive from the same Channels. Once one of them receives and executes, control is sent back to the Scheduler to allow other spawned Fibers to execute. They can keep "pinging" and "ponging" like this.

Speaking of ping-pong, you have this snippet from the "Go by Example" site:

package main
import "fmt"
func ping(pings chan<- string, msg string) {
    pings <- msg
}
func pong(pings <-chan string, pongs chan<- string) {
    msg := <-pings
    pongs <- msg
}
func main() {
    pings := make(chan string, 1)
    pongs := make(chan string, 1)
    ping(pings, "passed message")
    pong(pings, pongs)
    fmt.Println(<-pongs)
}

And the Crystal version:

def ping(pings, message)
  pings.send message
end
def pong(pings, pongs)
  message = pings.receive
  pongs.send message
end
pings = Channel(String).new
pongs = Channel(String).new
spawn ping pings, "passed message"
spawn pong pings, pongs
puts pongs.receive # => "passed message"

The Crystal version feels more natural and easy to read for me. I think most Rubyists will feel the same.

Crystal: Not Just for Rubyists

Crystal is a simple, easy-to-learn, high-performant, general programming language that uniquely combines all of these without any compromise. It's not just for Rubyists!

So what can you build with Crystal? Games, graphic renderers, low-level agents, web applications, and much more. It's really up to you what your next shiny project will be in Crystal! If you'd like to explore some projects built with Crystal, check out <crystalshards.xyz>. It's a great place to discover Crystal projects.

Bonus: If you can't find a name for your next project, try the name generator!

Resources

If you've made it this far, you might be asking, "Where do I go next?" Of course, the official Crystal book is a great place to start learning. Crystal for Rubyists is a free book to bootstrap your Crystal journey, and Crystal has an active Gitter room for communication. See you there!

Posts you may also find interesting:

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.