“Man wearing headphones at desk with window view of sunset in background” by Simon Abrams on Unsplash

I do the majority of my programming in ruby, and it’s a pleasure. The syntax is clean and without clutter. The standard library is full of powerful features. There’s a rich and mature community. What’s not to like?

The problems with ruby are well-documented: Performance is a concern and the concurrency model leaves something to be desired. Another problem I encountered was with ruby executables. I’ve always enjoyed writing small developer tools for myself in ruby, but since they are dependent on your ruby version, changing ruby versions can break executables or require them to be reinstalled (if you’re using something like rvm). This can become a problem when you work on an app using an old version of ruby, but your scripts (or their dependencies) use modern ruby features.

Enter Crystal! Claiming to be “Fast as C, slick as Ruby”, Crystal sounds like the performant, shareable, syntactically beautiful language I’m looking for. I decided to build a simple note-taking CLI app to introduce the language to myself. Here are some takeaways from that experience.

Compilation forces you to handle nils, preventing runtime errors

In my notes app, I wanted to open a file for quickly taking notes scoped to the current git branch. The idea is that when I create a feature branch, I’ll also have a notes file for that feature where I can quickly jot down relevant info. To accomplish this, I run git status and parse the results. The code started off the same in ruby and crystal (which happened a lot in this project):

private def git_branch_name

`git branch`.split("

").find { |branch| branch =~ /^\*/ }[2..-1]

end

This code just runs git branch, splits on newlines, and loops thru until it finds a line that starts with an * , which is how git marks your current branch. I expected it to work, but I got an error:

in /Users/mgregory/.mike/apps/notes/src/notes/filename_finder.cr:23: undefined method '[]' for Nil (compile-time type is (String | Nil)) `git branch`.split("

").find { |branch| branch =~ /^\*/ }[2..-1]

^

Crystal errors are formatted quite nicely.

The problem was that find does not guarantee a non-nil value, so calling :[] on it, could throw an error at runtime. At first, I was a little frustrated that I was being asked to handle this edge case immediately, but upon reflection, I realized that this was not an edge case, and I’d be getting runtime errors if I ran this code outside of a git init’d directory. If I were writing this in ruby, I would have realized this mistake much later and would have written a less robust application.

The solution was just to add a little nil handling:

private def git_branch_name

branch = `git branch`.split("

").find do |branch|

branch =~ /^\*/

end branch[2..-1] if branch

end

It’s worth noting that this method can still return nil, so further nil handling is required where it is called, but this made sense for my purposes since the public method calling it always returns a string.

Executable crystal binaries are awesome

In ruby, gems are the preferred way to share code. Gems are great, but they are less than ideal for sharing executable CLI apps since they are dependent on a compatible ruby version.

I’ve encountered this problems numerous times in the wild, since I regularly work on a ruby 2.1.7 application, but I regularly use tools from the ruby standard library that were introduced in later versions. In practice it looks like this:

$ my-cli-app # works

$ cd old_rails_app

$ my-cli-app # error

When I cd into the app, rvm (or rbenv) changes my current ruby version, a dependency of my CLI app.

With a compiled crystal binary, you don’t even need crystal to run the file. Just put the executable in a directory within your PATH environment variable and you’re good to go. Everything needed to run the app is in the binary.

It’s fast

It’s so much faster than ruby. I used a very simple example, but you can find much better benchmarks elsewhere. In ruby:

# testy.rb

arr = []

10_000.times do |n|

arr << n

end

p arr.size $ time ruby testy.rb

# 10000

# ruby testy.rb 0.03s user 0.01s system 79% cpu 0.054 total

In crystal:

# testy.cr

arr = [] of Int32

10_000.times do |n|

arr << n

end

p arr.size $ time ./testy

# 10000

# ./testy 0.00s user 0.00s system 75% cpu 0.009 total

Note that with crystal, I skipped the compilation step. To run a crystal file for debugging/development, you need to run:

$ crystal run my_file.cr

This will not be as fast as the optimized release version of your app. To build a file for release (the step I skippped):

$ crystal build my_file.cr --release

The built in Crystal CLI is feature-rich

As briefly mentioned in the previous section, crystal ships with a powerful CLI for building and running your apps. You will usually use it to build and run your apps, but there are two “hidden” features that I think every crystal developer should know about.

crystal docs

Running crystal docs from the root of a crystal app will generate full html/css/js pages for all of your classes in your app. You’ll have some TODOs to fill in, but overall this feature makes documentation much easier. The documentation pages are also searchable and includes the method signatures of your class and instance methods, what types they accept and return and the values of your constants.

crystal tool format

Who needs a linter? This tool reformats your code to fit crystal best practices. I imagine it’s very useful for maintaining code quality when working in teams. You never have to worry about that one developer who won’t look at the style guide again!

Conclusion

Overall, I found my experience working on this CLI in crystal to be a seamless transition coming from ruby. Crystal solved language ruby dependency problem, saved me from runtime errors, performed very well, and included some other tooling out of the box that made my life easier. I’m planning on continuing to build my other CLI apps in crystal and look forward to the language and frameworks using it to continue to grow. Crystal is a language to keep an eye on.