Note: This post was originally posted on marmelab.com.
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.
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('o')
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 you 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 'e' 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.
Top comments (7)
I have not fully read your article yet, but you win 'Headline of the year'.
lol ikr!
Awesome article! Thanks for writing it!
Have you had a chance to check out Elixir at all? I feel like you may find it interesting as well.
Thanks! ;)
Yes, I tried Elixir too :) Here is an article I wrote about Elixir Genserver.
marmelab.com/blog/2017/10/04/elixi...
Great write up,
One little correction though;
I think "That means Crystal needs to be compiled
onfor the platform where it'll be executed." is more correct.wow, awesome article! thank you so much for writing this <3
Thanks! :)