A Quick Dive into the Crystal Programming Language

Share this article

A Quick Dive into the Crystal Programming Language
Screenshot 2016-07-20 07.57.18

You might have heard mentions of the Crystal programming language of late. It is a language that looks very similar to Ruby. In fact, many Ruby programs are also valid Crystal programs. However, it must be emphasized that this is a mere side effect of the syntax of the language and is not a goal of the project.

One of the most interesting things about Crystal is that it is a statically type-checked language, yet it doesn’t require the programmer to sprinkle types everywhere like Java. Crystal compiles down to efficient code, which means Crystal programs are much faster than Ruby programs.

In this article we will take a quick dive into Crystal. This is by no means a comprehensive walk-through of all the features in Crystal. Instead, we are going to develop a concurrent Chuck Norris joke fetcher with the lens of a Rubyist. This involves making HTTP GET requests and also some JSON parsing.

We will see how far that takes us, along with looking into the facilities that Crystal provides that makes things more convenient.

Getting Crystal

I will gently direct you to the installation guide since it would undoubtedly do a far superior job at covering installation for the various Linux distributions, Mac OSX, and other Unix systems. For the MS Windows users out there: Sorry! Crystal requires a Linux/Unix based system.

The Sequential Version

We begin with the sequential version first. First, we need an HTTP client to fetch the jokes. Our data is going to come from The Internet Chuck Norris Database. In order to retrieve a joke, you just need to call out to the URL such as:

http://api.icndb.com/jokes/123

This returns:

{
  "type":"success",
  "value":{
    "id":123,
    "joke":"Some people wear Superman pajamas. Superman wears Chuck Norris pajamas.",
    "categories":[
    ]
  }
}

Let’s create a new class and name it chucky.cr. While we are at it, we shall implement Chucky#get_joke(id):

require "http/client"
require "json"

class Chucky
  def get_joke(id)
    response = HTTP::Client.get "http://api.icndb.com/jokes/#{id}"
    JSON.parse(response.body)["value"]["joke"]
  end
end

c = Chucky.new
puts c.get_joke(20)

Crystal comes built-in with an HTTP client and JSON parser. Let’s try running this in the terminal, which is done by passing the file name to the crystal command:

$ crystal chucky.cr
The Chuck Norris military unit was not used in the game Civilization 4, because a single Chuck Norris could defeat the entire combined nations of the world in one turn.

Great success! So far, so good. Say we want to retrieve a bunch of jokes. Seems pretty straightforward:

class Chucky
  ...other methods...

  def get_jokes(ids : Array(Int32))
    ids.map do |id|
      get_joke(id)
    end
  end

end

The first thing you’ll notice that’s different from Ruby is that the type of ids is being explicitly defined as Array(Int32). This is read as “an Array of Int32s”. To be clear, we could have left this out. However, since I’m pretty sure that ids are always Int32s, I want to be extremely clear and avoid mistakes such as:

c = Chucky.new
puts c.get_jokes(["20"])

In fact, when you try running this, you’ll get a compile-time error:

Error in ./chucky.cr:50: no overload matches 'Chucky#get_jokes' with type Array(String) Overloads are:
 - Chucky#get_jokes(ids : Array(Int32))
puts c.get_jokes(["20"])
   ^~~~~~~~~

The astute reader would point out that it makes way more sense to specify the type on the argument in Chucky#get_joke instead, and she would be absolutely right. In fact, you can also specify the return type of the method:

class Chucky

  def get_joke(id : Int32) : String
    # ...
  end

  def get_jokes(ids : Array(Int32)) : Array(String)
    # ...
  end

end

c = Chucky.new
puts c.get_jokes([20]) # <-- Change this back to an Array(Int32)

Let’s try again:

% crystal chucky.cr

Error in ./chucky.cr:53: instantiating 'Chucky#get_jokes(Array(Int32))'

puts c.get_jokes([20])
       ^~~~~~~~~

in ./chucky.cr:20: instantiating 'get_joke(Int32)'

      get_joke(id)
      ^~~~~~~~

in ./chucky.cr:24: type must be String, not JSON::Any

  def get_joke(id : Int32) : String
      ^~~~~~~~

Whoops! The compiler caught something! So it seems like Chucky#get_joke(id) doesn’t return a String, but instead it returns a JSON::Any. This is great, because the compiler has caught one of our bad assumptions.

Now we are left with two choices. Either we switch our Chucky#get_joke[s] to return JSON::Any or we continue using String. My vote is for String, because any client code shouldn’t care that the jokes are of JSON::Any. Let’s modify the Chucky#get_joke(id). For good measure, we also handle the case where there’s some parsing error and simply return an empty String:

class Chucky

  # ...

  def get_joke(id : Int32) : String
    response = HTTP::Client.get "http://api.icndb.com/jokes/#{id}"
    JSON.parse(response.body)["value"]["joke"].to_s rescue ""
  end

end

Everything should run fine now. We have a slight problem, though. Try to imagine what happens when we do this:

c = Chucky.new
puts c.get_jokes[20, 30, 40]

In our current implementation, the jokes will be fetched sequentially. This means that the time take for Chucky#get_jokes(ids) to complete is the total time taken to fetch all three jokes.

We can do better!

The Concurrent Version

Now that we have made it work, let’s make it fast. Crystal comes with a concurrency primitive called fibers, which are basically a lighter-weight version of threads. The other concurrency primitive are channels. If you have done any Golang, this is basically the same idea. Channels are a way for fibers to communicate without the headaches of shared memory, locks, and mutexes.

We are going to use both fibers and channels to concurrently fetch the jokes.

Here’s the main idea. We will create a channel in the main fiber. Each call to Chucky#get_joke(id) will be done in a fiber. Once the joke is fetched, we will then send the channel the result.

class Chucky
  # ...

  def get_jokes(ids : Array(Int32)) : Array(String)

    # 1. Create channel in the main fiber.
    chan = Channel(String).new

    # 2. Execute get_joke in a fiber. Send the result to the channel.
    ids.each do |x|
      spawn do
        chan.send(get_joke(x))
      end
    end

    # 3. Receive the results.
    (1..ids.size).map do |x|
      chan.receive
    end
  end
end

First, we create a channel. Note that we need to specify the type of the channel.

Next, we execute get_joke(id) in a fiber. This is done in a spawn block. Once we get a result from get_joke(id), we send the results to the previously created channel.

Sending a channel a value is only one piece of the puzzle. In order to get a value out of a channel we need to call Channel#receive. Each time we call receive we get back one value. If there are not values (yet), it will block. Since we know the size of ids, we just need to call Channel#receive ids.size times.

Try running the program again. This time, the total time taken is around the time taken for the longest request.

Crystal Yay’s

Crystal looks really impressive. I like that it is statically typed, yet the type system doesn’t get in the way, especially since you can mostly do without specifying the types.

Being a compiled language (in LLVM no less), it is not surprising that some of the performance benchmarks I’ve seen are very impressive.

I like fibers and channels as concurrency primitives. I hope there are more to come. There are a few other language features like macros and structs that make it stand out from Ruby.

Crystal Meh’s

There is no REPL. For a Rubyist, this feels almost unimaginable to do any Ruby programming without the faithful IRB (or Pry).

While there is concurrency, there is no parallelism yet. This means that a Crystal program only uses one core. However, it’s still a young language so this is not the final story.

Finally, the Crystal community has the uphill task of creating a thriving ecosystem, such as Rubygems.

Thanks for Reading!

I hope you enjoyed this quick dive into Crystal. If you are interested in learning more, I encourage you to read through the documentation.

Happy Crystalling!

Frequently Asked Questions about Crystal Programming Language

What makes Crystal Programming Language unique compared to other languages?

Crystal Programming Language is unique due to its syntax, which is similar to Ruby, making it easy to read and write. It is statically typed and compiled, which means it checks the types of variables at compile time, reducing runtime errors. Crystal also has a powerful macro system, which allows for metaprogramming. It is also capable of calling C code by writing bindings to it in Crystal, which makes it highly versatile.

How does Crystal Programming Language handle concurrency?

Crystal handles concurrency using a model called “Fibers”. These are lightweight, independent execution units that can be scheduled by the programmer, rather than the operating system. This allows for efficient and easy-to-understand concurrent programming.

Can I use Crystal for web development?

Yes, Crystal is a great choice for web development. It has several frameworks, such as Kemal, Amber, and Lucky, which provide a robust set of tools for building web applications. These frameworks offer features like routing, middleware support, and database integrations.

How does Crystal ensure type safety?

Crystal is a statically typed language, which means it checks the types of all variables at compile time. This helps to catch potential type errors before the program is run, increasing the reliability of the code.

What is metaprogramming in Crystal?

Metaprogramming in Crystal is achieved through its powerful macro system. Macros in Crystal are code that generates other code during compilation. This allows for reducing boilerplate code and creating domain-specific languages.

How can I call C code in Crystal?

Crystal has a built-in way to bind to existing C libraries. You can write bindings to C code directly in Crystal, and then call those functions as if they were written in Crystal. This makes Crystal a versatile language that can leverage the vast ecosystem of C libraries.

What are some good resources to learn Crystal Programming Language?

There are several excellent resources to learn Crystal. The official Crystal Language website provides a comprehensive guide and API documentation. Other resources include online tutorials, blogs, and books like “Crystal Programming” by Packt Publishing.

How does Crystal handle memory management?

Crystal uses automatic garbage collection for memory management. This means that the programmer does not have to manually allocate and deallocate memory, reducing the chance of memory leaks and other memory-related errors.

Can I use Crystal for system programming?

Yes, Crystal can be used for system programming. It has low-level features, like pointers and inline assembly, which are typically required for system programming. However, it also provides high-level abstractions, making it easier to write system programs compared to traditional system programming languages.

How is the performance of Crystal compared to other languages?

Crystal is a compiled language, which generally leads to better performance compared to interpreted languages. It also has features like native concurrency support and direct bindings to C libraries, which can further enhance performance. However, the actual performance can vary depending on the specific use case and how the code is written.

Benjamin Tan Wei HaoBenjamin Tan Wei Hao
View Author

Benjamin is a Software Engineer at EasyMile, Singapore where he spends most of his time wrangling data pipelines and automating all the things. He is the author of The Little Elixir and OTP Guidebook and Mastering Ruby Closures Book. Deathly afraid of being irrelevant, is always trying to catch up on his ever-growing reading list. He blogs, codes and tweets.

crystalGlennG
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week