Learn Crystal in a Day

Crystal is a programming language invented by the last Gelfling to stop evil muppets from taking over his planet. It strongly resembles Ruby and shares many of its characteristics. It is statically typed and compiles to super fast native code. Basically it is a dynamic modern language but is also fast.

It looks like this:

require "http/server"

server = HTTP::Server.new(8080) do |context|
  context.response.content_type = "text/plain"
  context.response.print "Cats are amazing!"
end

puts "Listening on http://127.0.0.1:8080"
server.listen

Checkout Crystal In Five Minutes to learn how to setup a Crystal dev environment.

The Crystal Paradigm

In Crystal every thing is an object. The numbers are objects:

42.class  #=> Int32

True and false are objects:

true.class #=> Bool

So are classes:

class Cats
end
Cats.class #=> Class

Even nil is an object:

nil.class  #=> Nil

In Crystal you will rarely encounter API that isn't a method call. For example, there is no new keyword in Crystal. To create a new instance of an object we call a method named new like this:

myCat = Cat.new

The API for primitive data types like strings or arrays, are also methods:

cat_name = "Ferguson"
cat_name.upcase #=> FERGUSON

cats = ['tabby', 'tiny']
puts cats.first #=> 'tabby'

This object oriented approach keeps the mental surface area of Crystal really tiny. Learning Crystal is often just a matter of learning the methods you can call.

The way we build Crystal programs is through objects and composition of those objects. We can construct a basic object like this:

class Cat
  def talk
    return "meow"
  end
end
cat = Cat.new()

This model becomes really powerful when we use libraries or frameworks. Instead of learning the API of an external system, we import it and compose it into our own objects. For example, if we want to build a web app we could require an external lib called Kemal and import it into our project:

require "kemal"
get "/" do
  "Hello World!"
end

Kemal.run

Primitive Data types

Let's take a look at all the primitive types we have available in Crystal. We have our fundamental value types; booleans and nil:

# This is a comment btw

nil   #=> false : Bool
true  #=> true : Bool
false #=> false : Bool

Two things are false in Crystal; nil and false. That is it. Everything else is true.

Numbers

We have integers:

# Integers
42.class #=> Int32

# Signed integer types
1_i8.class  #=> Int8
1_i16.class #=> Int16
1_i32.class #=> Int32
1_i64.class #=> Int64

Floats:

# Floats
1.0.class #=> Float64

# There 64bit and 32bit floating types
1e10.class    #=> Float64
1.5e10.class  #=> Float64
1.5e-7.class  #=> Float64
1.0_f32.class #=> Float32
1_f32.class   #=> Float32

And finally we have a few other number type thingys:

# Binary numbers
0b1101 #=> 13 : Int32

# Octal numbers
0o123 #=> 83 : Int32

# Hexadecimal numbers
0xFE012D #=> 16646445 : Int32
0xfe012d #=> 16646445 : Int32

Chars and Strings

Chars are done using single quotes like this:

'c'.class #=> Char

And strings use double quotes:

"cats".class #=> String

Strings are immutable and every time you add to one you are actually creating an entirely new string:

cats = "Ferguson"   #=> ​"Ferguson"          : String
cats.object_id      #=> 4523706304          : UInt64
cats += ", Crystal" #=> "Ferguson, Crystal" : String
cats.object_id      #=> 4402814464          : UInt64

You can place vars in a string using #{var} like this:

greatest_cat = "Ferguson"
"The greatest cat is #{greatest_cat}" #=> "The greatest cat is Ferguson"

You can declare a multiline string by just letting it span multiple lines:

"this is a
      multline string
      it just goes
      on and on my friend
      some people started quoting it
"

If you have a string with some double quotes you can wrap the string in %() instead of quotes:

%(hello "world")

Symbols

Crystal has immutable constants just like ruby:

:cats.class #=> Symbol
greatest_cat == :cat? #=> true  : Bool

Arrays

Crystal has arrays:

[1, 2, 3, 4].class #=> Array(Int32)
[1, "gargleblaster", "infinite phaser"].class #=> Array(Int32 | String)

You need to make sure that you specify a type for an empty array because Crystal cant do type inference on an empty array:

cats = [] # Syntax error in :1: for empty arrays use '[] of ElementType'
cats = [] of String      #=> [] : Array(String)

Arrays have indexes:

cats = ["Ferguson", "Snowball", "McFluffypants"]
cats[0] #=> "Ferguson" : String

You can use a range as an index:

array = [1, 2, 3, 4, 5] #=> [1, 2, 3, 4, 5] : Array(Int32)
array[2, 3] #=> [3, 4, 5]
array[1..3] #=> [2, 3, 4]

We can add to an array using <<:

array << 6  #=> [1, 2, 3, 4, 5, 6]

And remove items using pop and shift:

# Remove from the end of the array
array.pop #=> 6
array     #=> [1, 2, 3, 4, 5]

# Remove from the beginning of the array
array.shift #=> 1
array       #=> [2, 3, 4, 5]

You can check if an item exists in an array using includes?:

array.includes? 3 #=> true

If your array only consists of strings or symbols you can use a special syntax to eliminate those pesky commas and quotes:

%w(Ferguson Sardine) #=> ["Ferguson", "Sardine"] : Array(String)
%i(Ferguson Sardine) #=> [:Ferguson, :Sardine]   : Array(Symbol)

Ranges

You can create ranges of numbers with dots:

1..10                  #=> Range(Int32, Int32)
Range.new(1, 10).class #=> Range(Int32, Int32)

If you use two dots the range is inclusive, with three dots it is exclusive:

(3..5).to_a  #=> [3, 4, 5]
(3...5).to_a #=> [3, 4]

Hashes

Hashes exist in Crystal. You can use any type as a key:

{1 => 2, 3 => 4}.class                     #=> Hash(Int32, Int32)
{ :cats => ["Ferguson", "Sardine"] }.class #=> Hash(Symbol, Array(String))

You must specify a type for empty hashes (just like with arrays):

{}                     # Syntax error
{} of Int32 => Int32   # {}

You can lookup elements in a hash using []:

cats = {
  :Ferguson => {
    :weight => "fat"
  }
}
cats[:Ferguson][:weight] #=> "fat" : String

colors = {"green" => "frog"}
colors["green"] #=> "frog"

numbers = {1 => 2, 3 => 4}
numbers[1] #=> 2

Looking up a hash a non-existent key will throw an error:

hash = { "dogs" => "are awesome" }
hash["no_such_key"]  #=> Missing hash key: "no_such_key" (KeyError)

You can get nil instead of an error by using a question mark:

hash = { "dogs" => "are awesome" }
hash["no_such_key"]? #=> nil

Tuples

Tuples are a fixed size, immutable, ordered list of values. They are constructed like this:

{1, "cats", 2, 'c'} #=> Tuple(Int32, String, Int32, Char)

You can access values by index:

cats = {"Ferguson", "Sardine"}
cats[0] #=> "Ferguson"

Accessing a value that doesn't exist will throw an error:

cats = {"Ferguson", "Sardine"}
cats[2] #=> syntax error : Index out of bound

To check for the existence of a value by index use a question mark:

cats = {"Ferguson", "Sardine"}
cats[2]? #=> nil

Lambdas/Procs

Crystal is not a very functional language but it does provide procs. Procs are objects with a call method. (If you ask some programmers, they will tell you that is all a function is). Procs give you the ability to pass functions around and change their context. You can create one like this:

talk = ->(name: String) { "meow! my name is #{name}" }
talk.call("Ferguson") #=> "meow! my name is Ferguson" : String

Flow Control

Crystal has all the typical control statements. They are closed with the end keyword.

Crystal has ifs which look like:

if true
  "cats are awesome"
elsif false
  "dogs are awesome"
else
  "all animals are awesome"
end

You can inline an if:

puts "cats are awesome" if true

It is important to remember that Crystal is a language with types, this means that control flow statements will influence how Crystal infers types. A var’s type depends on the type of the expressions used:

if a.is_a? String
  a.class #=> String
end

if a < 4625
  a = "mysterious"
else
  a = true
end
# a : String | Bool

b = 1
if cats_are_awesome
  b = "cats"
end
# b : Int32 | String

if can be used as an expression:

a = if 2 > 1
      3
    else
      4
    end

a #=> 3

And as a ternary operator:

a = 1 > 2 ? 3 : 4 #=> 4

Case Statements

Crystal has case/switch statements:

tasks = "meow"

action = case cmd
  when "meow"
    "meowing"
end

action #=> "meowing"

Loops

Crystal has loops for looping your loopy stuff. There are three forms; while, until, and each. each is the most common one, as the enumerable protocol uses that form. When you have an enumerable object, it will provide an each method which expects a block to pass each item to:

(1..3).each do |Index|
  puts "Number: #{num}"
end
# "Number: 1"
# "Number: 2"
# "Number: 3"

The while form looks like this:

index = 1
while index <= 3
  puts "Index: #{index}"
  index += 1
end
# Index: 1
# Index: 2
# Index: 3

And the until form:

index = 1
until index > 3
  puts "Index: #{index}"
  index += 1
end
# Index: 1
# Index: 2
# Index: 3

Functions

Functions in Crystal look like this:

def double(x)
  x * 2
end

def cats
  return "are awesome"
end

You can call them like this:

double 2  #=> 4 : Int32
double(4) #=> 8 : Int32
cats      #=> "are awesome" : String

Parentheses are optional when the call is unambiguous

def sum(x, y)
  x + y
end

sum 3, 4 #=> 7

sum sum(3, 4), 5 #=> 12

Functions can have question marks:

def are_cats_awesome?
  return true
end
are_cats_awesome?

And exclamation marks:

def cats_are_awesome!
  "cats are awesome!"
end
cats_are_awesome!

The standard convention is to use exclamation marks when a method will modify the object instead of returning a new one:

name = "Mr. Ferguson"
name.gsub "Mr.", "Dr."  #=> "Dr. Ferguson"

# The value of name is still "Mr. Ferguson"
name #=> "Mr. Ferguson"

# But if we use gsub! instead it gets changed
name.gsub! "Mr.", "Dr."  #=> "Dr. Ferguson"
name #=> "Dr. Ferguson"

Blocks and yield

All functions/methods have an implicit block parameter that we can execute with yield:

def surround
  puts '{'
  yield
  puts '}'
end

surround { puts "hello world" }

# Outputs
# {
# hello world
# }

Blocks can be used like procs and have a call method:

def cats(&block)
  block.call "are awesome"
end

Classes

You define a class with the class keyword:

class Cat
  # A static class variable
  @@species = "hallway speeders"

  # an instance var with type
  @name : String

  def initialize(@name)
  end

  # Setter for name
  def name=(name)
    @name = name
  end

  # Getter for name
  def name
    @name
  end

  # We can declare both getters and setters in one shot with `property`
  property :name

  # We can also declare getters and setters individually
  getter :name
  setter :name

  # An instance method
  def talk(msg)
    puts "I'm talking from an instance #{msg}"
  end

  # A class method
  def self.talk(msg)
    puts "I'm talking from a class #{msg}"
  end
end

We can use it like this:

ferguson = Cat.new("Ferguson")

And we can call the methods like this:

ferguson.species #=> "hallway speeders"
ferguson.name #=> "Ferguson"
ferguson.talk #=> "I'm talking from an instance"
Cat.talk #=> "I'm talking from a class"

We can extend a class like this:

class AdorableCreature < Cat
end

Modules

Modules are a way encapsulating a bunch of shared methods:

module CatsModule
  def talk
    "I'm a cat and I'm awesome"
  end
end

You can include these methods onto a class instance with the include keyword:

class Cat
  include CatsModule
end

ferguson = Cat.new
ferguson.talk #=> "I'm a cat and I'm awesome"

You can add the same methods to a class by extending it:

class Cat
  extend CatsModule
end
Cat.talk #=> "I'm a cat and I'm awesome"

Exception handling

Crystal has try/catch like exception handling. Wrap the code the code that might error in a begin statement and then catch it with rescue:

exception = begin
  raise Exception.new
rescue ex1 : IndexError
  puts ex1
  "Index error"
rescue ex2 : MyException | MyAnotherException
  puts ex2
  "Custom exception"
rescue ex3 : Exception
  puts ex3
  "Default exception"
rescue ex4
  "catch all". # the last rescue is the equivalent of a "finally" statement.
end
exception #=> "Default exception"

You can define a custom exception simply by extending exception:

class MyException < Exception
end

Destructing

Crystal supports destructuring arrays and tuples. You can destructure an array like this:

one, two, three = [1,2,3]
one   #=> 1
two   #=> 2
three #=> 3

It works on functions to:

def cats
  ["Ferguson", "Sardine"]
end
ferguson, sardine = cats
ferguson #=> "Ferguson"

Works on tuples too:

def cats
  {"Ferguson", "Sardine"}
end
ferguson, sardine = cats
ferguson #=> "Ferguson"

Splats

Splats are like destructuring but the reverse. It can take arguments and convert them into an array:

def cats(*array)
  array.each { |cats| puts cats }
end

That does it! You should have a decent ground in the basics of Crystal now! Be sure to checkout awesome-crystal for a bunch more resources.