Robert Mosolgo

Crystal First Impressions

The Crystal programming language combines Ruby-like syntax with a really powerful compiler. As a result, it’s fun to write, fast to run, and hard to screw up!

My Crystal experience so far:

I’d say it’s a combination of:

  • a more-stable-Ruby (like Elixir, but without Erlang)
  • a developer-friendly, life-embetter-ing type system (like Elm, but … not JavaScript)
  • a real compiler! (like C, but fun to read and write)

Um, what else could you want?! (See last paragraph 😛)

Crystal Syntax

Crystal brings the best of Ruby:

  • Concise literals, just like Ruby (take it for granted until you use regexps in Python 🙀)
  • Great OO support, classes & modules just like Ruby
  • Attractive syntax thanks to blocks, operator overloading and optional parens
  • consistent, predictable standard library (like Ruby)

Plus, some improvements over Ruby:

  • Method overloading
  • Python-like keyword args: must have default value, may be passed as kwargs or positional args (I could go either way on this since Ruby 2.1, but it beats options={})
  • More robust Proc literals, reminded me of Elixir
  • Convention: ? methods return maybe-nil types, while their counterparts raise on nil
  • First-class enums & tuples
  • Immutable strings, like Ruby 3 will have (?)

For completeness, you lose some things from Ruby:

  • Runtime code creation, like define_method & friends
  • Runtime code evaluation, like eval & friends

Crystal offers a powerful macro system that makes up for the loss of runtime metaprogramming. Unlike C preprossing, Crystal macros are awesome. You basically define functions which are called at compile-time, then generate code with liquid-like syntax.

Crystal Typing

Inferring Types

Crystal infers types from your code, so these are OK:

my_string = "Hello World"
# String
my_hash = {key: "value", key2: "value2"}
# Hash(Symbol, String)
my_array = [1,2,3]
# Array(Int32)

When types mix, Crystal automatically unions them. It will ensure any usages of the variable in question are valid for both types. For example:

my_variable = "string"
my_variable = 1
# String | Int32

# Ok, because String & Int32 both implement #to_f
my_variable.to_f

# You can add runtime checks to call type-specific methods
if my_variable.is_a?(String)
  my_variable.upcase
end

There are some times you need to define types to help the compiler. For example, there aren’t any values here to tell the compiler what to expect:

some_array =  [] of Int32
# You can use custom types, too
some_hash =   {} of Symbol => SomeCustomClass

Goodbye, NoMethodErrors

If you’re like me, you hate this:

undefined method `whatever' for nil:NilClass

Something somehow became nil. 😢

Instead, Crystal reads your code, and if there’s somewhere a value could be nil, it throws a compile error:

in ./src/lisp/binding.cr:55: undefined method 'find_owner' for Nil

      @parent.find_owner(key)
              ^~~~~~~~~~

You have two options:

  • Add an explicit not-nil check (if object.is_a?(String) ...) so the compiler knows it will be safe
  • Refactor so the value won’t be nil

Of course, the first one seems better at the start, but I hope to get better at the second one 😁.

What’s Missing?

Crystal really shows its youth. Its shortcomings all fall in that vein:

  • Poorly documented, which isn’t so bad if you’re coming from Ruby
  • Few projects out there (I think the package repository is a free Heroku app)
  • Standard library has some kinks, they say it is still changing

One example of a standard library kink is the handling of break, next and return in blocks. If you want to exit a block early, you have to choose one of those three. The problem is that, to choose the right one, you have to know whether the method captures the block into a proc or simply yields values to it. It’s a drag to have to know a method’s implementation to call it! (IRL, I didn’t run into this and I suspect it would be easy enough to work around it.)

Now What?

I really liked Crystal and I hope I can work with it more!