Let's cook some Crystal!

Julien Demangeon
Julien DemangeonDecember 14, 2017
#golang#performance#popular

Crystal? Some of you may have heard about it in the best TV series of all the time, Breaking Bad! Thankfully, we're not gonna talk about drugs in this post, but about a fabulous programming language I've heard of recently.

Breaking Bad Crystal Cooking

In my constant quest to discover new languages and paradigms, I came across Crystal. Crystal is a compiled, statically typed, and object-oriented language. Its syntax is strongly inspired by Ruby, and its goals are mostly the same as Golang.

As usual when I learn a new language, I started a fresh new project that I could publish on GitHub. My use case: a CLI app intended to facilitate Caesar Cipher operations.

During this post, I'll try to give a large overview of the language capabilities, together with code snippets.

Ruby, Are You There?

As I said in the introduction, a lot of the Crystal syntax concepts were taken from Ruby. The main reason of this syntax choice comes from the language creators' background.

Ruby is an object-oriented, reflective and dynamically typed programming language. It was developed 20 years ago to provide productivity and fun to developers. It was used in many medium and large projects like GitHub, Diaspora or Redmine (my first happy Ruby experience), and made popular by RoR (Ruby on Rails).

While Ruby is an interpreted and multi-platform language (which runs on YARV VM), Crystal is not. That means Crystal needs to be compiled on the platform where it'll be executed.

As you are going to see later, Crystal is not Ruby, it mimics Ruby. If you're a Rubyist, you'll be a little disappointed by static typing and inference. Even so, many basic concepts have been taken from Ruby. Among them, we find "Blocks", "Symbols" and "Everything is an object" principle.

Crystal Blocks

Blocks are heavily used in Crystal (as much as in Ruby). They allow to capture a block of code with its own context, and execute it later. It's almost the same thing as closures or anonymous functions in other languages.

Here is a part of code from my toy project.

(1...Encoder::ALPHABET.size).each do |shift|
    decoded = Encoder.decode(encoded, shift)
    candidates << { key: decoded, count: ((decoded.split(' ') - [""]) & dictionary).size }
end

In this example, the block which is defined between do |shift| and end is called for each letter between 1 and Encoder::ALPHABET.size (thanks to Crystal Range).

The last letter (defined by Encoder::ALPHABET.size) is excluded from the range because of .... To include the last letter, it would been necessary to use .., which is inclusive.

Within the .each call, the block is "yielded" with the corresponding function from range.cr. In fact, a block is called with the yield keyword from Crystal as described in the doc example below.

def twice
  yield
  yield
end

twice do
  puts "Hello!"
end

# => prints "Hello!" twice

Symbols

The Symbol is another interesting concept also found in Ruby. A Crystal Symbol is a constant which is identified by its name. Internally, Crystal holds a registry of Symbols, which are represented as an Int32.


:hello === :hello # true
:hello === "hello" # false

Symbols are closely the same as Elixir atoms.

Everything is an Object

In Crystal, as in Ruby, everything is an object. Effectively, since reading post, you've already experienced no less than 3 objects (Blocks, Symbols and Ranges). Just like in Ruby, all objects extend Object. When you write Crystal code, most of the time, you write classes.

module Curses
  class Window
  end
end

Curses::Window.new

However, Crystal can also be used in a functional way. As a module requires a class, the solution is to make the module "self-extended". For instance, I can write an Encoder module that does not contain a class by using extend self:

module Encoder
  extend self

  ALPHABET = "abcdefghijklmnopqrstuvwxyz "

  def encode(uncoded : String, shift : (Int32 | Int64))
      dictionary = create_shift_dictionary(shift)
      normalized_uncoded = normalize(uncoded)

      normalized_uncoded.tr(dictionary.keys.join, dictionary.values.join)
  end

  [...]

end

Now I can call the encode() method directly, without a supporting class:

include Encoder
encode('hello')

Crystal Functionalities

Whereas Crystal picks some concepts from Ruby, it also comes with a lot of unexpected functionalities such as Type inference, concurrency and compile time null reference checks.

Type Inference at its Core

Type inference is one of the most valuable Crystal functionnalities. It gives you the power to delegate the type attribution process to the compiler. Moreover, in association with Union Types, it allows to give different types to the same variable.

if 1 + 2 == 3
  a = 1
else
  a = "hello"
end

a # : Int32 | String

In the example above, the type of a is automaticaly infered from the AST (Abstract Syntax Tree) at compile time. It's also possible to assign the type manually, as shown below.

a : (Int32 | String) = 1

Type Inference also works with Arrays, Tuples, NamedTuples, Hashes...

[1, "hello", 'x'] # Array(Int32 | String | Char)
{1, "hello", 'x'} # Tuple(Int32, String, Char)
{name: "Crystal", year: 2017} # NamedTuple(name: String, year: Int32)
{1 => 2, 'a' => 3} # Hash(Int32 | Char, Int32)

Syntaxic sugar and shortcuts exist to declare empty typed structures:

[] of Int32 # same as Array(Int32).new
Tuple(Int32, String, Char) # Tuple(Int32, String, Char)
NamedTuple(x: Int32, y: String) # NamedTuple(x: Int32, y: String)
{} of Int32 => Int32 # same as Hash(Int32, Int32).new

Concurrency with Channels and Fibers

In addition to its fun syntax and static typing system, Crystal brings some concurrency capabilities (inspired by CSP) through Fibers. It's one of the most valuable functionalities of Crystal in comparison to other compiled languages

Concurrency consists of running several tasks in the same time interval through operation switching. This concept is often mistaken with parallelism, which consists of running multiples operations simultaneously (mathematically, in parallel).

Crystal Fibers can be compared to Golang's Goroutines. The main difference lays in the way Crystal allocate operations to CPUs. When Golang uses all availables CPUs, Crystal is only capable of using one of them for the moment.

Fibers use the same message passing system as Golang to achieve synchronisation and data passing between Fibers. It uses a kind of light-weight pipes called Channels.

Here is a little example of asynchronous operations written with Goroutines and Fibers. It'll give you a more concrete example of Fibers usage.

// GOLANG

messages := make(chan string)
defer close(messages)

go func() {
    time.Sleep(time.Second * 5)
    messages <- "ping"
}()

msg := <-messages // Program will wait for 5s
fmt.Println(msg) // => ping
# CRYSTAL

messages = Channel(String).new

spawn do
  sleep 5.seconds
  messages.send("ping")
end

msg = messages.receive # Program will wait for 5s
puts msg # => ping

In this example, I used the channel system to provide an exchange pipe between the main thread and the routine (async thread).

To declare an async thread, the spawn keyword with an associated code block is used on the Crystal side. With Golang, an anonymous function is started in a goroutine via the go keyword ahead of it.

At assignation time (in this example), the main thread is stopped until the corresponding channel sends data back.

Null Reference Checks

In your career, you've certainly heard of "NullPointerException" or the The Billion Dollar Mistake. This well known concept comes from the fact that it's deadly simple to call on a reference which is Null.

In Crystal, this kind of exception can't happen. A checks is issued at compile time to prevent it, thanks to Static Typing.

if rand(42) > 0
  hello = "hello world"
end

puts hello.upcase

The previous code will fail with a compile time exception.

$ crystal hello_world.cr
Error in hello_world.cr:5: undefined method 'upcase' for Nil (compile-time type is (String | Nil))

puts hello.upcase
          ^~~~~~

This exception occurs because hello can be either String or Nil, and there's no upcase method on Nil.

Crystal Tooling

Another argument in favour of Crystal is its great standard library, which covers most of the usual needs. A lot of tools are bundled with Crystal itself (HTTP, JSON, Markdown Parser and so on), making it ready to use instantly.

For example, here is a small HTTP server written in Crystal.

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://127.0.0.1:8080"
server.listen

Crystal Ecosystem

Despite its youth, Crystal has gained a lot of popularity in no time thanks to its Ruby-like syntax. It actually shares hype in the declining rubyist's world with another language, Elixir.

Most Crystal-based projects are very confidential for the moment. Nevertheless, some of them have gained popularity rapidly:

  • KEMAL - Fast, Effective, Simple web framework (including websockets, etc)
  • CRECTO - Database wrapper (inspired by Elixir's Ecto)
  • SIDEKIQ - Simple, efficient job processing (Including web monitoring UI)

Crystal bundles with an easy-to-use dependency management system. It consists of a simple YAML file (named shard.yml) listing dependencies and associated versions. It looks like the following:

name: MyAmbitiousProject
version: 0.1.0

dependencies:
  openssl:
    github: datanoise/openssl.cr
    branch: master

development_dependencies:
  minitest:
    git: https://github.com/ysbaddaden/minitest.cr.git
    version: ~> 0.3.1

license: MIT

This way, a simple crystal deps command is enough to retrieve needed dependencies into the project tree.

If you're curious to discover other interesting Crystal projects, a dedicated portal has been created specially for that purpose.

Conclusion

Crystal has been first released in 2014. It's a relatively young language for the moment, but it promises a bright future.

Its advanced type inference system combined with static typing and union types give Crystal the feeling of a higher-level scripting language, with performances close to the metal. It can be a good alternative to Golang for modest performance needs.

Moreover, if you're already familiar with Ruby, Crystal is the first choice language. It offers good performances without having to bear Golang's heavy syntax.

Finally, if you like this language but need distributed operations, you can take a look at the Elixir language, which is very appreciated by rubyists, too.

If you're interested in the project associated to this post, you should take a look at the repository. It'll also give you a good start to dockerize Crystal apps in the same time.

Did you like this article? Share it!