As a long time Rubyist I’ve been intrigued by the Crystal language for a while. Crystal is a compiled statically typed language that uses Ruby syntax pretty much wherever it can. Now the Crystal language approaches a 1.0 release later this year and I wanted to try it out.

After skimming the language reference much of Crystal’s syntax seems familiar. One thing was sure to be different though: Crystal is statically typed. Yet, a lot of example code looks just like Ruby. It’s deceptively dynamic looking. So I figured this was a good aspect of the language to focus on first, to ensure I understood what’s going on with types in Crystal.

Type Inference by Default

The example code on the Crystal language home page looks remarkably like Ruby. How can Crystal support this while doing good type-checking?

I dived in and started reading the reference. I quickly hit on an at first puzzling behavior.

In Ruby you could do


# Assign if 'x' is nil or define and assign  if 'x' is undefined
x ||= 25
=> 25

# Because 'x' is now assigned to 25, it won't be re-assigned
x ||= 17
=> 25

In Crystal you can’t use the ||= operator before a variable is defined, not surprisingly.

In example.cr:79:6

 79 | z ||= 99
           ^
Error: '||=' before definition of 'z'

However, you can use ||= with a defined variable:

y = nil
y ||= 25
puts "y is #{y}"
y ||= 95
puts "y is #{y}"

Which, when executed, gives:

y is 25
y is 25

…as you’d expect from ||=. But hang on! Isn’t Crystal supposed to be statically typed? Can you just assign a different type of value (integer) to an already defined variable (Nil)?

Yes, sort of. In this case the compiler can deduce that you’ve assigned two different types to the variable ‘y’: Nil and Int32, and lets you re-define the variable. In the general case Crystal notices you have assigned values of different types and creates a “union” type; but in the simple case illustrated here the compiler is smart enough to know it never needs to use both types at the same time, so it simplifies the type to what it is after the last assignment.

The typeof() function in Crystal gives the type of a variable. Using the typeof() function shows this:

y = nil
puts "y is type: #{typeof(y)}"
y ||= 25
puts "y is type: #{typeof(y)}"
puts "y is #{y}"
y ||= 95
puts "y is #{y}"

Giving:

y is type: Nil
y is type: Int32
y is 25
y is 25

The ‘y’ variable is only one type at a time. In a case where a variable might be assigned to either type the compiler will infer a union type and not optimize it away:

r = rand(25)
a = if r > 5
	"Hello"
else
	29
end
puts "type of 'a': #{typeof(a)}"

Gives:

type of 'a': (Int32 | String)

To sidestep the Crystal compiler’s type inference you can tell it how to type your values with the as() method. Remember that like Ruby, everything is an object including literals, so you can call as() on number literals:

big_number = 99.as(Int128)
postcode =   "7EW2E6".as(String | Int32)

Crystal tries to mimic the behavior of Ruby: Variables can be re-assigned to values of different types. The code will type check methods on variables with the current type at that point in the code; once a variable is re-assigned to another type it will be type-checked for that type going forward.

This is different from most traditional strongly typed languages like Go or C++ or Java. If you have made the compiler infer a “union” type it will be type-checked and fail to compile if you call a method available to only one of the two types in the union.

You can do a certain amount of semi-dynamically typed logic in Crystal with the is_a and responds_to? methods on objects, so that at runtime you can control the program depending on types and behavior. Given Crystal’s ability to re-assign the same variable with differently typed data I think this is a must-have.

Types as Variable Values

One more twist (that’s not really a twist if you’re a Ruby programmer): You can assign types to variables as their values. Clearly this is a very powerful capability, but it can be confusing at first. I accidentally assigned the type ‘Nil’ to ‘x’ in my example above at first.

x = Nil
x ||=25
I somehow typed a capital ‘N’ on Nil. But ‘Nil’ is “truthy” so the ‘   =’ won’t ever re-assign ‘x’. ‘Nil’ is a class with only one value: ‘nil’.

Aside from reassignments Crystal will type-check your code. Just keep this fact in mind, especially if you’re not coming from a Ruby background.

Static Type Assignment

A less Ruby-like way to assign your variables involves giving the type explicitly

a_number: Int64 = 20
a_name: String = "Colin"

NOTE: Be sure to put a space after the “:” symbol! Before re-reading the reference I didn’t realize the significance of the whitespace after “:” and couldn’t understand why the compiler kept rejecting my code!

You prevent the compiler from reassigning variables to different types with the explicit type assignment. the compiler will error on attempts to re-assign to a different type of value:

a_number: Int64 = = 5
a_number = 8
a_number = "abc"

when compiled gives something like:

Showing last frame. Use --error-trace for full trace.

In example.cr:103:1

 103 | a_number = "abc"
       ^-------
Error: type must be Int64, not (Int64 | String)