[001.2] Crystal Language basics

Learning about the playground, types, method overloading, classes and modules, generics, and macros.

Subscribe now

Crystal Language basics [08.21.2017]

Hello again! In this episode, we will walk through Crystal's basic constructs, its built-in types, and its type system. For the sake of convenience, we will work on the Crystal playground, which allows you to write, compile and run code in a simple web page.

To start the playground, run:

crystal play

And open your browser at <localhost:8080>.

As you can see, we can type a simple program here, and it gets compiled and run on the spot:

1 + 1

On the right, the playground shows the line-by-line execution of the program, along with the data type of each expression. The basic types are the ones you would expect: booleans, strings, symbols, characters, and numeric types, including signed and unsigned integers and floats of different sizes.

true
"hello"
:symbol
'a'
1
1_i8
1_u16
1_i64
1.0_f32
1.0

Crystal also has lists, represented by the Array class, and their contents are strictly typed as well; this means that you cannot have just a list, your list needs to have a specific type, such as integers.

array = [1,2,3]

In this case, Crystal infers that the Array contains integers. If we mix different types, like this, then the Array will contain a union of integers and strings. As you become more familiar with the language, you’ll notice that union types are one of the most powerful features of Crystal.

array = [1,2,3,"four"]
elem = array[0]

We can restrict the type of a variable in a context by using the is_a? method. Note that inside the if branch, elem is shown as an integer, not as an integer or a string.

if elem.is_a?(Int32)
  elem
end

In Crystal, all types are non-nilable by default. If we add a nil to our array, note that its type changes to a union that includes the nil class.

array = [1,2,3,nil]
elem = array[0]

Crystal will keep us from invoking methods on a nilable object without checking.

elem + 1

That has an error. We can avoid the error by checking for nil:

if elem
  elem + 1
end

Also, note that if we declare an empty Array, the compiler has no way to know what it contents will be, so it forces us to declare its type.

[]
[] of Int32

The same applies to dictionaries, called hashes in Crystal, just like in Ruby.

hash = {"key" => 42}
hash["key"]
empty = {} of String => Int32

Crystal also inherits from Ruby the concept of blocks, which are used much along the standard library, in particular in functional higher-order functions such as map and select..

[1,2,3,4].map {|x| x * 3}
[1,2,3,4].map { |x| x * 3 }.select { |x| x.odd? }

We can simplify this expression using ranges.

(1..4).map {|x| x * 3}.select { |x| x.odd? }

And further, by using the ampersand shorthand syntax for simple blocks.

(1..4).map {|x| x * 3}.select(&.odd?)

In fact, blocks are so used in Crystal that they are also used for iteration. Crystal supports both the do...end syntax and the curly braces for defining blocks

(1..4).map {|x| x * 3}.select(&.odd?).each do |x|
  puts x
end

Last but not least, Crystal also has tuples and named tuples, which are a great way to quickly define complex types without resorting to classes.

t = {1, "foo"}
t[0]
nt = {number: 1, text: "foo"}
nt[:number]

Note that tuples and named tuples are both immutable.

t = {1, "foo"}
t[0] = 10

Crystal also has Pointer as a basic data type for interacting with low-level non-managed code, though that goes beyond the scope of this episode.

Now, even though Crystal is statically typed, the types of most expressions are inferred by the compiler. For instance, let’s define a twice function. Functions in Crystal are declared with the def keyword, like in Ruby.

def twice(a)
  a + a
end

If we invoke this function with a number, the compiler will correctly infer the resulting type to be a number as well:

twice(2)

Same if we use a string, where the result is the concatenation:

twice("foo")

BUT the compiler will complain if we invoke it with a type that does not support the plus operator:

twice(true)

Under the hood, the compiler is checking for all usages of your twice function, and compiling a version for ints and another for strings, and statically linking to the appropriate one in each call. This makes the execution much faster.

If you want to specify the argument types for a function, you can do it like:

def twice(a : Int32)
  a + a
end

twice(2)

So now if we try to invoke it with a string, we get an error from the compiler.

twice("foo")

We can define multiple overloads of the function, for different argument types, and Crystal will pick the right one in each case.

def twice(a : String)
  "#{a}, #{a}"
end

By the way, this is how you interpolate strings in Crystal; again, same syntax as Ruby.

So, in this chapter we reviewed Crystal's basic data types and constructs, and took a small peek into type inference, type restrictions and method overloading. In the next episode we’ll go one step further and work with classes, generics, and macros. Thanks for watching