Crystal is not Ruby Part 1

Filipe Correia
Runtime Revolution
Published in
6 min readMay 10, 2018

--

Crystal is a very interesting project that I have been following for the last couple of months. There is no 1.0 release yet (but it’s close) so I haven’t really used it in any production systems, but its newness makes it a very good language to get into open source.

If you check out their site, the tag line for Crystal is “Fast as C, slick as ruby”. Basically it has a very similar syntax to Ruby, there is even a whole section in the wiki called “Crystal for Rubyists”.

I really like the language, it’s definitely one of my favorites, and there are a lot of things that feel familiar to me when writing Crystal — it feels like I’m writing Ruby. But once in a while there are some things I run into that break that illusion.

Sometimes these are helpful things, other times they become a nuisance, as in all things, there are good and bad parts. I decided to just talk about the bad in this post.

However, because I want to convince people to try out Crystal, there will also be a part 2 where I talk about the things that make Crystal awesome.

Thing #1: You have to run the compiler

The first big difference from Ruby is that Crystal is a compiled language. This is part of why it’s much faster than Ruby but it does mean a slower development experience as you have to run the compiler every time you want to test something. This is not a workflow I’m used to anymore.

Now, if you’re working on a web application by using something like the lucky framework you won’t notice much of a difference. Though the compilation is a bit slow, it still handles that automatically, so the experience is pretty similar to something in Rails.

But if you’re building tools using Crystal, something that is as simple as running a script in Ruby involves having to compile and then running the resulting binary. (Note: you can run code directly but the command line can get messy.)

To be fair the compiler is pretty fast. For instance, compiling the compiler (yeah the Crystal compiler is built in Crystal), which is a pretty big project, takes 20 seconds.

Thing #2: Enumerable has less stuff

The Enumerable module in Ruby is awesome, it gives a lot of useful collection methods and its included in Array, Hash which makes it a must check docs page whenever you’re working with hashes or arrays.

In Crystal you don’t have just the Enumerable module. For reasons that I haven’t dug into, there are more modules, the methods that you usually find in enumerable are spread out. For example, the Array class includes Iterable, Iterator and Indexable. This takes a bit of getting used to when looking through the docs, and it definitely requires you to know a bit about the concerns that were behind Iterable, Iterator and Indexable. Even after you get used to that part there are a still couple of changes to what you’d expect to find in an array’s API.

And to add to the confusion, there are a bunch of changes to the methods you’d expect to find. The include? method is includes? . The methods collect, keep_if, delete_if, take and some other methods that usually have an alias that does the same thing in Ruby don’t exist in Crystal.

Thing #3: Hashes and Array are Generic

In the context of a statically typed language this makes sense. This does imply that you declare empty array’s and hashes like so:

If the array or hash can have nils you do it by appending ? to the type like this:

And finally if the array or hash can have more then one type you can use a union type by joining the types with |:

Thing #4: It forces you to deal with nil

Since it’s a compiled language with a type system there are a lot of situations where you might have nil values so a variable will have the type Nil|OtherType. This means that any time you want to call a method on an object that might be nil you get a compilation error.

The error goes away by either:

  • Creating an if/switch statement to create a branch where you deal with the possibility of a nil value.
  • Go the ballsy way and just call .not_nil! on the object.

Now this isn’t necessarily a bad thing but if you’re not careful in the way you map out some things it can get annoying pretty fast.

Also there are a few edge cases that the compiler doesn’t catch, like:

Thing #5: Low level concerns

You need to care about low level stuff which can be kind of overwhelming. One simple example is when to pick between struct and class , as per the documentation the difference between a struct and a class is:

  • Invoking new on a struct allocates it on the stack instead of the heap
  • A struct is passed by value while a class is passed by reference
  • A struct implicitly inherits from Struct, which inherits from Value. A class implicitly inherits from Reference.
  • A struct cannot inherit a non-abstract struct.

When I look at stuff like this in the docs I get PTSD from my days in uni when I had to care about these things in C.

Thing #6: There is no reflection

Instead of reflection there are macros. You can do some pretty crazy stuff with reflection in Ruby, you can also do some pretty crazy stuff with macros in Crystal but in a different manner.

Macros are functions that run in the first phases of compilation and generate new code that is later compiled along with your program. It effectively puts the macro code in place wherever you call it.

Two simple things you need to know about macros:

  1. It uses a special syntax of double curly braces{{}} to distinguish between code that is being interpolated and code that stays as is.
  2. The macro receives an AST nodes as the arguments which means you need to be a bit familiar how those work.

This makes writing macros a little clunky, while in Ruby you’re just writing Ruby code and it’s easy to forget you’re doing meta programming, in Crystal the macros just look different from the rest of the code and feels like you’re just doing something wrong. For example to achieve something like this snippet in Ruby:

You write it like this:

And you need to be careful what variable names you use because you can access a variable that is defined outside of the scope of the macro. So in a macro you can either assume there is a variable defined where the macro is interpolated (the compiler errors if there isn’t).

Or you can use a special syntax to declare a fresh variable by prepending % to the variable (the compiler makes sure to create a new variable name). For example:

I am very new to Crystal, and I think most of the things I’m complaining about in this article won’t bother me as much in the future, but I hope these serve as a stepping stone for anyone that tries it out coming from Ruby.

These are minor gripes more then anything else, so don’t let these dissuade you from trying out this awesome language.

A couple of links

Have you tried using crystal yet? Do you like to look into new languages? We’d love to hear from you, just comment below!

I work at Runtime Revolution from sunny Lisbon developing great products. If you’d like to work with us just reach out.

--

--